diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c4f6e2bd0f..92ea9273f1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://bevyengine.org/donate/ +custom: https://bevy.org/donate/ diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.md b/.github/ISSUE_TEMPLATE/docs_improvement.md index 4bc84c5fc9..f4b6f2019e 100644 --- a/.github/ISSUE_TEMPLATE/docs_improvement.md +++ b/.github/ISSUE_TEMPLATE/docs_improvement.md @@ -10,4 +10,4 @@ assignees: '' Provide a link to the documentation and describe how it could be improved. In what ways is it incomplete, incorrect, or misleading? -If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevyengine.org/learn/contribute/helping-out/writing-docs/) instead. +If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevy.org/learn/contribute/helping-out/writing-docs/) instead. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37db848558..526ac4c401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: - name: CI job # To run the tests one item at a time for troubleshooting, use # cargo --quiet test --lib -- --list | sed 's/: test$//' | MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-disable-weak-memory-emulation" xargs -n1 cargo miri test -p bevy_ecs --lib -- --exact - run: cargo miri test -p bevy_ecs + run: cargo miri test -p bevy_ecs --features bevy_utils/debug env: # -Zrandomize-layout makes sure we dont rely on the layout of anything that might change RUSTFLAGS: -Zrandomize-layout @@ -247,7 +247,7 @@ jobs: - name: Check wasm run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort env: - RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory -D warnings" + RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory" markdownlint: runs-on: ubuntu-latest @@ -293,7 +293,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.32.0 + uses: crate-ci/typos@v1.33.1 - name: Typos info if: failure() run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a04fadc94..e732cbe66d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -82,7 +82,7 @@ jobs: - name: Finalize documentation run: | echo "" > target/doc/index.html - echo "dev-docs.bevyengine.org" > target/doc/CNAME + echo "dev-docs.bevy.org" > target/doc/CNAME echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt rm target/doc/.lock diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 91a98f3ea7..485861ebdf 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -49,7 +49,8 @@ jobs: --exclude ci \ --exclude errors \ --exclude bevy_mobile_example \ - --exclude build-wasm-example + --exclude build-wasm-example \ + --exclude no_std_library - name: Create PR uses: peter-evans/create-pull-request@v7 diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 96a287981d..87df34b932 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -43,5 +43,5 @@ jobs: repo: context.repo.repo, body: `**Welcome**, new contributor! - Please make sure you've read our [contributing guide](https://bevyengine.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` + Please make sure you've read our [contributing guide](https://bevy.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` }) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73bc77c455..887318f22a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ # Contributing to Bevy If you'd like to help build Bevy, start by reading this -[introduction](https://bevyengine.org/learn/contribute/introduction). Thanks for contributing! +[introduction](https://bevy.org/learn/contribute/introduction). Thanks for contributing! diff --git a/Cargo.toml b/Cargo.toml index a3d3a2ab63..e6c75611ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "bevy" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" categories = ["game-engines", "graphics", "gui", "rendering"] description = "A refreshingly simple data-driven game engine and app framework" exclude = ["assets/", "tools/", ".github/", "crates/", "examples/wasm/assets/"] -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" keywords = ["game", "engine", "gamedev", "graphics", "bevy"] license = "MIT OR Apache-2.0" repository = "https://github.com/bevyengine/bevy" documentation = "https://docs.rs/bevy" -rust-version = "1.85.0" +rust-version = "1.86.0" [workspace] resolver = "2" @@ -133,6 +133,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -163,6 +164,7 @@ default = [ "vorbis", "webgl2", "x11", + "debug", ] # Recommended defaults for no_std applications @@ -245,6 +247,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +# Provides raytraced lighting (experimental) +bevy_solari = [ + "bevy_internal/bevy_solari", + "bevy_asset", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_render", +] + # Provides sprite functionality bevy_sprite = [ "bevy_internal/bevy_sprite", @@ -291,8 +302,8 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] -# Use the configurable global error handler as the default error handler. -configurable_error_handler = ["bevy_internal/configurable_error_handler"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -496,7 +507,10 @@ file_watcher = ["bevy_internal/file_watcher"] embedded_watcher = ["bevy_internal/embedded_watcher"] # Enable stepping-based debugging of Bevy systems -bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] +bevy_debug_stepping = [ + "bevy_internal/bevy_debug_stepping", + "bevy_internal/debug", +] # Enables the meshlet renderer for dense high-poly scenes (experimental) meshlet = ["bevy_internal/meshlet"] @@ -537,30 +551,36 @@ libm = ["bevy_internal/libm"] # Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures. web = ["bevy_internal/web"] +# Enable hotpatching of Bevy systems +hotpatching = ["bevy_internal/hotpatching"] + +# Enable collecting debug information about systems and components to help with diagnostics +debug = ["bevy_internal/debug"] + [dependencies] -bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } +bevy_internal = { path = "crates/bevy_internal", version = "0.17.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } # Wasm does not support dynamic linking. [target.'cfg(not(target_family = "wasm"))'.dependencies] -bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-features = false, optional = true } +bevy_dylib = { path = "crates/bevy_dylib", version = "0.17.0-dev", default-features = false, optional = true } [dev-dependencies] rand = "0.8.0" rand_chacha = "0.3.1" -ron = "0.8.0" +ron = "0.10" flate2 = "1.0" serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = "1.0.140" bytemuck = "1.7" -bevy_render = { path = "crates/bevy_render", version = "0.16.0-dev", default-features = false } +bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. -bevy_ecs = { path = "crates/bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_state = { path = "crates/bevy_state", version = "0.16.0-dev", default-features = false } -bevy_asset = { path = "crates/bevy_asset", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "crates/bevy_reflect", version = "0.16.0-dev", default-features = false } -bevy_image = { path = "crates/bevy_image", version = "0.16.0-dev", default-features = false } -bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.16.0-dev", default-features = false } +bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } +bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "crates/bevy_reflect", version = "0.17.0-dev", default-features = false } +bevy_image = { path = "crates/bevy_image", version = "0.17.0-dev", default-features = false } +bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.17.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" async-std = "1.13" @@ -572,7 +592,7 @@ hyper = { version = "1", features = ["server", "http1"] } http-body-util = "0.1" anyhow = "1" macro_rules_attribute = "0.2" -accesskit = "0.18" +accesskit = "0.19" nonmax = "0.5" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] @@ -585,6 +605,17 @@ ureq = { version = "3.0.8", features = ["json"] } wasm-bindgen = { version = "0.2" } web-sys = { version = "0.3", features = ["Window"] } +[[example]] +name = "context_menu" +path = "examples/usage/context_menu.rs" +doc-scrape-examples = true + +[package.metadata.example.context_menu] +name = "Context Menu" +description = "Example of a context menu" +category = "Usage" +wasm = true + [[example]] name = "hello_world" path = "examples/hello_world.rs" @@ -1245,6 +1276,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug category = "3D Rendering" wasm = false +[[example]] +name = "solari" +path = "examples/3d/solari.rs" +doc-scrape-examples = true +required-features = ["bevy_solari"] + +[package.metadata.example.solari] +name = "Solari" +description = "Demonstrates realtime dynamic raytraced lighting using Bevy Solari." +category = "3D Rendering" +wasm = false # Raytracing is not supported on the web + [[example]] name = "spherical_area_lights" path = "examples/3d/spherical_area_lights.rs" @@ -2061,6 +2104,7 @@ wasm = false name = "dynamic" path = "examples/ecs/dynamic.rs" doc-scrape-examples = true +required-features = ["debug"] [package.metadata.example.dynamic] name = "Dynamic ECS" @@ -2215,7 +2259,6 @@ wasm = false name = "fallible_params" path = "examples/ecs/fallible_params.rs" doc-scrape-examples = true -required-features = ["configurable_error_handler"] [package.metadata.example.fallible_params] name = "Fallible System Parameters" @@ -2227,7 +2270,7 @@ wasm = false name = "error_handling" path = "examples/ecs/error_handling.rs" doc-scrape-examples = true -required-features = ["bevy_mesh_picking_backend", "configurable_error_handler"] +required-features = ["bevy_mesh_picking_backend"] [package.metadata.example.error_handling] name = "Error handling" @@ -3422,6 +3465,28 @@ description = "An example for CSS Grid layout" category = "UI (User Interface)" wasm = true +[[example]] +name = "gradients" +path = "examples/ui/gradients.rs" +doc-scrape-examples = true + +[package.metadata.example.gradients] +name = "Gradients" +description = "An example demonstrating gradients" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "stacked_gradients" +path = "examples/ui/stacked_gradients.rs" +doc-scrape-examples = true + +[package.metadata.example.stacked_gradients] +name = "Stacked Gradients" +description = "An example demonstrating stacked gradients" +category = "UI (User Interface)" +wasm = true + [[example]] name = "scroll" path = "examples/ui/scroll.rs" @@ -3510,6 +3575,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" @@ -3893,6 +3969,16 @@ description = "A simple 2D screen shake effect" category = "Camera" wasm = true +[[example]] +name = "2d_on_ui" +path = "examples/camera/2d_on_ui.rs" +doc-scrape-examples = true + +[package.metadata.example.2d_on_ui] +name = "2D on Bevy UI" +description = "Shows how to render 2D objects on top of Bevy UI" +category = "Camera" +wasm = true [package.metadata.example.fps_overlay] name = "FPS overlay" @@ -4361,3 +4447,48 @@ name = "Extended Bindless Material" description = "Demonstrates bindless `ExtendedMaterial`" category = "Shaders" wasm = false + +[[example]] +name = "cooldown" +path = "examples/usage/cooldown.rs" +doc-scrape-examples = true + +[package.metadata.example.cooldown] +name = "Cooldown" +description = "Example for cooldown on button clicks" +category = "Usage" +wasm = true + +[[example]] +name = "hotpatching_systems" +path = "examples/ecs/hotpatching_systems.rs" +doc-scrape-examples = true +required-features = ["hotpatching"] + +[package.metadata.example.hotpatching_systems] +name = "Hotpatching Systems" +description = "Demonstrates how to hotpatch systems" +category = "ECS (Entity Component System)" +wasm = false + +[[example]] +name = "core_widgets" +path = "examples/ui/core_widgets.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets] +name = "Core Widgets" +description = "Demonstrates use of core (headless) widgets in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "core_widgets_observers" +path = "examples/ui/core_widgets_observers.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets_observers] +name = "Core Widgets (w/Observers)" +description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" +category = "UI (User Interface)" +wasm = true diff --git a/README.md b/README.md index be1bcf6bfe..db0a3d57b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevyengine.org) +# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevy.org) [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) [![Crates.io](https://img.shields.io/crates/v/bevy.svg)](https://crates.io/crates/bevy) @@ -13,7 +13,7 @@ Bevy is a refreshingly simple data-driven game engine built in Rust. It is free ## WARNING -Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevyengine.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevyengine.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. +Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevy.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevy.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. **MSRV:** Bevy relies heavily on improvements in the Rust language and compiler. As a result, the Minimum Supported Rust Version (MSRV) is generally close to "the latest stable release" of Rust. @@ -29,15 +29,15 @@ As a result, the Minimum Supported Rust Version (MSRV) is generally close to "th ## About -* **[Features](https://bevyengine.org):** A quick overview of Bevy's features. -* **[News](https://bevyengine.org/news/)**: A development blog that covers our progress, plans and shiny new features. +* **[Features](https://bevy.org):** A quick overview of Bevy's features. +* **[News](https://bevy.org/news/)**: A development blog that covers our progress, plans and shiny new features. ## Docs -* **[Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. +* **[Quick Start Guide](https://bevy.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. * **[Bevy Rust API Docs](https://docs.rs/bevy):** Bevy's Rust API docs, which are automatically generated from the doc comments in this repo. * **[Official Examples](https://github.com/bevyengine/bevy/tree/latest/examples):** Bevy's dedicated, runnable examples, which are great for digging into specific concepts. -* **[Community-Made Learning Resources](https://bevyengine.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. +* **[Community-Made Learning Resources](https://bevy.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. ## Community @@ -46,11 +46,11 @@ Before contributing or participating in discussions with the community, you shou * **[Discord](https://discord.gg/bevy):** Bevy's official discord server. * **[Reddit](https://reddit.com/r/bevy):** Bevy's official subreddit. * **[GitHub Discussions](https://github.com/bevyengine/bevy/discussions):** The best place for questions about Bevy, answered right here! -* **[Bevy Assets](https://bevyengine.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. +* **[Bevy Assets](https://bevy.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. ### Contributing -If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevyengine.org/learn/contribute/introduction)**. +If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevy.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! @@ -58,9 +58,9 @@ For more complex architecture decisions and experimental mad science, please ope ## Getting Started -We recommend checking out the [Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction) for a brief introduction. +We recommend checking out the [Quick Start Guide](https://bevy.org/learn/quick-start/introduction) for a brief introduction. -Follow the [Setup guide](https://bevyengine.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. +Follow the [Setup guide](https://bevy.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. Once set up, you can quickly try out the [examples](https://github.com/bevyengine/bevy/tree/latest/examples) by cloning this repo and running the following commands: ```sh @@ -75,7 +75,7 @@ To draw a window with standard functionality enabled, use: ```rust use bevy::prelude::*; -fn main(){ +fn main() { App::new() .add_plugins(DefaultPlugins) .run(); @@ -84,7 +84,7 @@ fn main(){ ### Fast Compiles -Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevyengine.org/learn/quick-start/getting-started/setup). +Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevy.org/learn/quick-start/getting-started/setup). ## [Bevy Cargo Features][cargo_features] @@ -96,7 +96,7 @@ This [list][cargo_features] outlines the different cargo features supported by B Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the [Rust gamedev ecosystem](https://arewegameyet.rs/), and the many libraries we build on. -A huge thanks to Bevy's [generous sponsors](https://bevyengine.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevyengine.org/donate/) if you like what we're building. +A huge thanks to Bevy's [generous sponsors](https://bevy.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevy.org/donate/) if you like what we're building. This project is tested with BrowserStack. diff --git a/assets/branding/bevy_solari.svg b/assets/branding/bevy_solari.svg new file mode 100644 index 0000000000..65b996493f --- /dev/null +++ b/assets/branding/bevy_solari.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/textures/food_kenney.png b/assets/textures/food_kenney.png new file mode 100644 index 0000000000..a5fe374eba Binary files /dev/null and b/assets/textures/food_kenney.png differ diff --git a/benches/benches/bevy_ecs/change_detection.rs b/benches/benches/bevy_ecs/change_detection.rs index 92f3251abc..3cfa5bcbed 100644 --- a/benches/benches/bevy_ecs/change_detection.rs +++ b/benches/benches/bevy_ecs/change_detection.rs @@ -49,6 +49,7 @@ impl BenchModify for Table { black_box(self.0) } } + impl BenchModify for Sparse { fn bench_modify(&mut self) -> f32 { self.0 += 1f32; diff --git a/benches/benches/bevy_ecs/events/iter.rs b/benches/benches/bevy_ecs/events/iter.rs index dc20bc3395..9ad17ed8c8 100644 --- a/benches/benches/bevy_ecs/events/iter.rs +++ b/benches/benches/bevy_ecs/events/iter.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); pub struct Benchmark(Events>); diff --git a/benches/benches/bevy_ecs/events/send.rs b/benches/benches/bevy_ecs/events/send.rs index fa996b50aa..be8934e789 100644 --- a/benches/benches/bevy_ecs/events/send.rs +++ b/benches/benches/bevy_ecs/events/send.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); impl Default for BenchEvent { diff --git a/benches/benches/bevy_ecs/iteration/heavy_compute.rs b/benches/benches/bevy_ecs/iteration/heavy_compute.rs index 3a3e350637..e057b20a43 100644 --- a/benches/benches/bevy_ecs/iteration/heavy_compute.rs +++ b/benches/benches/bevy_ecs/iteration/heavy_compute.rs @@ -45,7 +45,6 @@ pub fn heavy_compute(c: &mut Criterion) { let mut system = IntoSystem::into_system(sys); system.initialize(&mut world); - system.update_archetype_component_access(world.as_unsafe_world_cell()); b.iter(move || system.run((), &mut world)); }); diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_system.rs b/benches/benches/bevy_ecs/iteration/iter_simple_system.rs index 2b6e828721..903ff08194 100644 --- a/benches/benches/bevy_ecs/iteration/iter_simple_system.rs +++ b/benches/benches/bevy_ecs/iteration/iter_simple_system.rs @@ -37,7 +37,6 @@ impl Benchmark { let mut system = IntoSystem::into_system(query_system); system.initialize(&mut world); - system.update_archetype_component_access(world.as_unsafe_world_cell()); Self(world, Box::new(system)) } diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 65c15f7308..808c3727d5 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -61,14 +61,10 @@ pub fn event_propagation(criterion: &mut Criterion) { group.finish(); } -#[derive(Clone, Component)] +#[derive(Event, EntityEvent, Clone, Component)] +#[entity_event(traversal = &'static ChildOf, auto_propagate)] struct TestEvent {} -impl Event for TestEvent { - type Traversal = &'static ChildOf; - const AUTO_PROPAGATE: bool = true; -} - fn send_events(world: &mut World, leaves: &[Entity]) { let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap(); @@ -117,6 +113,6 @@ fn add_listeners_to_hierarchy( } } -fn empty_listener(trigger: Trigger>) { +fn empty_listener(trigger: On>) { black_box(trigger); } diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 85207624e8..9c26b074e5 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,8 +1,8 @@ use core::hint::black_box; use bevy_ecs::{ - event::Event, - observer::{Trigger, TriggerTargets}, + event::{EntityEvent, Event}, + observer::{On, TriggerTargets}, world::World, }; @@ -13,7 +13,7 @@ fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) } -#[derive(Clone, Event)] +#[derive(Clone, Event, EntityEvent)] struct EventBase; pub fn observe_simple(criterion: &mut Criterion) { @@ -46,7 +46,7 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener_base(trigger: Trigger) { +fn empty_listener_base(trigger: On) { black_box(trigger); } diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 7b1cc29457..bedfb8e5af 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -62,6 +62,31 @@ pub fn spawn_commands(criterion: &mut Criterion) { group.finish(); } +pub fn nonempty_spawn_commands(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("nonempty_spawn_commands"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + for entity_count in [100, 1_000, 10_000] { + group.bench_function(format!("{}_entities", entity_count), |bencher| { + let mut world = World::default(); + let mut command_queue = CommandQueue::default(); + + bencher.iter(|| { + let mut commands = Commands::new(&mut command_queue, &world); + for i in 0..entity_count { + if black_box(i % 2 == 0) { + commands.spawn(A); + } + } + command_queue.apply(&mut world); + }); + }); + } + + group.finish(); +} + #[derive(Default, Component)] struct Matrix([[f32; 4]; 4]); diff --git a/benches/benches/bevy_ecs/world/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index e35dc999c2..7158f2f033 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -17,6 +17,7 @@ criterion_group!( benches, empty_commands, spawn_commands, + nonempty_spawn_commands, insert_commands, fake_commands, zero_sized_commands, diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index a95cb4a821..70f3cb6703 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -32,7 +32,7 @@ fn segment_ease(c: &mut Criterion) { fn curve_position(c: &mut Criterion) { /// A helper function that benchmarks calling [`CubicCurve::position()`] over a generic [`VectorSpace`]. - fn bench_curve( + fn bench_curve>( group: &mut BenchmarkGroup, name: &str, curve: CubicCurve

, diff --git a/clippy.toml b/clippy.toml index 2c98e8ed02..372ffbaf0b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -41,7 +41,6 @@ disallowed-methods = [ { path = "f32::asinh", reason = "use bevy_math::ops::asinh instead for libm determinism" }, { path = "f32::acosh", reason = "use bevy_math::ops::acosh instead for libm determinism" }, { path = "f32::atanh", reason = "use bevy_math::ops::atanh instead for libm determinism" }, - { path = "criterion::black_box", reason = "use core::hint::black_box instead" }, ] # Require `bevy_ecs::children!` to use `[]` braces, instead of `()` or `{}`. diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 759cf3e787..70ee16cf77 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_a11y" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides accessibility support for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "accessibility", "a11y"] @@ -40,13 +40,13 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } # other -accesskit = { version = "0.18", default-features = false } +accesskit = { version = "0.19", default-features = false } serde = { version = "1", default-features = false, features = [ "alloc", ], optional = true } diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 94468c148c..22b2f71f07 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -26,7 +26,8 @@ use accesskit::Node; use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - prelude::{Component, Event}, + component::Component, + event::{BufferedEvent, Event}, resource::Resource, schedule::SystemSet, }; @@ -44,7 +45,7 @@ use serde::{Deserialize, Serialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`. -#[derive(Event, Deref, DerefMut)] +#[derive(Event, BufferedEvent, Deref, DerefMut)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct ActionRequest(pub accesskit::ActionRequest); diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 11e819806c..adf17a8580 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -1,38 +1,38 @@ [package] name = "bevy_animation" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides animation functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "petgraph", ] } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } # other petgraph = { version = "0.7", features = ["serde-1"] } -ron = "0.8" +ron = "0.10" serde = "1" blake3 = { version = "1.0" } downcast-rs = { version = "2", default-features = false, features = ["std"] } diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index 688011a32c..593ca04d2e 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -55,7 +55,7 @@ pub struct CubicKeyframeCurve { impl Curve for CubicKeyframeCurve where - V: VectorSpace, + V: VectorSpace, { #[inline] fn domain(&self) -> Interval { @@ -179,7 +179,7 @@ pub struct WideLinearKeyframeCurve { impl IterableCurve for WideLinearKeyframeCurve where - T: VectorSpace, + T: VectorSpace, { #[inline] fn domain(&self) -> Interval { @@ -289,7 +289,7 @@ pub struct WideCubicKeyframeCurve { impl IterableCurve for WideCubicKeyframeCurve where - T: VectorSpace, + T: VectorSpace, { #[inline] fn domain(&self) -> Interval { @@ -406,7 +406,7 @@ fn cubic_spline_interpolation( step_duration: f32, ) -> T where - T: VectorSpace, + T: VectorSpace, { let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp; value_start * (coeffs.x * lerp + 1.0) @@ -415,7 +415,7 @@ where + tangent_in_end * step_duration * lerp * coeffs.w } -fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( +fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( width: usize, first: &'a [T], second: &'a [T], diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aa6d252fee..a5f4041ac7 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -1,10 +1,11 @@ //! The animation graph, which allows animations to be blended together. use core::{ + fmt::Write, iter, ops::{Index, IndexMut, Range}, }; -use std::io::{self, Write}; +use std::io; use bevy_asset::{ io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 21ea15f96f..ae7ce42ed6 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Animation for the game engine Bevy @@ -324,13 +324,13 @@ impl AnimationClip { .push(variable_curve); } - /// Add a untargeted [`Event`] to this [`AnimationClip`]. + /// Add an [`EntityEvent`] with no [`AnimationTarget`] 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) { + pub fn add_event(&mut self, time: f32, event: impl EntityEvent + Clone) { self.add_event_fn( time, move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { @@ -339,7 +339,7 @@ impl AnimationClip { ); } - /// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// Add an [`EntityEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds) /// is reached in the animation. @@ -349,7 +349,7 @@ impl AnimationClip { &mut self, target_id: AnimationTargetId, time: f32, - event: impl Event + Clone, + event: impl EntityEvent + Clone, ) { self.add_event_fn_to_target( target_id, @@ -360,19 +360,19 @@ impl AnimationClip { ); } - /// Add a untargeted event function to this [`AnimationClip`]. + /// Add an event function with no [`AnimationTarget`] to this [`AnimationClip`]. /// /// 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`]. + /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event`]. /// See also [`add_event_to_target`](Self::add_event_to_target). /// /// ``` /// # 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}"); + /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}"); /// }) /// ``` pub fn add_event_fn( @@ -388,14 +388,14 @@ impl AnimationClip { /// 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`]. + /// For a simpler [`EntityEvent`]-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}"); + /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}"); /// }) /// ``` pub fn add_event_fn_to_target( @@ -1534,7 +1534,7 @@ mod tests { use super::*; - #[derive(Event, Reflect, Clone)] + #[derive(Event, EntityEvent, Reflect, Clone)] struct A; #[track_caller] diff --git a/crates/bevy_anti_aliasing/Cargo.toml b/crates/bevy_anti_aliasing/Cargo.toml index 5a8e48ecb5..8c32d70fff 100644 --- a/crates/bevy_anti_aliasing/Cargo.toml +++ b/crates/bevy_anti_aliasing/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_anti_aliasing" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides various anti aliasing implementations for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -16,17 +16,17 @@ smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } # other tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs index 707d75819d..0b4a99fb59 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs @@ -1,5 +1,5 @@ use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, @@ -95,20 +95,12 @@ impl ExtractComponent for ContrastAdaptiveSharpening { } } -const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle = - weak_handle!("ef83f0a5-51df-4b51-9ab7-b5fd1ae5a397"); - /// Adds Support for Contrast Adaptive Sharpening (CAS). pub struct CasPlugin; impl Plugin for CasPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE, - "robust_contrast_adaptive_sharpening.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "robust_contrast_adaptive_sharpening.wgsl"); app.register_type::(); app.add_plugins(( @@ -171,6 +163,7 @@ impl Plugin for CasPlugin { pub struct CasPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, + shader: Handle, } impl FromWorld for CasPipeline { @@ -194,6 +187,7 @@ impl FromWorld for CasPipeline { CasPipeline { texture_bind_group, sampler, + shader: load_embedded_asset!(render_world, "robust_contrast_adaptive_sharpening.wgsl"), } } } @@ -217,7 +211,7 @@ impl SpecializedRenderPipeline for CasPipeline { layout: vec![self.texture_bind_group.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_anti_aliasing/src/experimental/mod.rs b/crates/bevy_anti_aliasing/src/experimental/mod.rs deleted file mode 100644 index a8dc522c56..0000000000 --- a/crates/bevy_anti_aliasing/src/experimental/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Experimental rendering features. -//! -//! Experimental features are features with known problems, missing features, -//! compatibility issues, low performance, and/or future breaking changes, but -//! are included nonetheless for testing purposes. - -pub mod taa { - pub use crate::taa::{TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasing}; -} diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_aliasing/src/fxaa/mod.rs index 4848d3d268..6b914c4e86 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/fxaa/mod.rs @@ -1,5 +1,5 @@ use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, @@ -80,13 +80,11 @@ impl Default for Fxaa { } } -const FXAA_SHADER_HANDLE: Handle = weak_handle!("fc58c0a8-01c0-46e9-94cc-83a794bae7b0"); - /// Adds support for Fast Approximate Anti-Aliasing (FXAA) pub struct FxaaPlugin; impl Plugin for FxaaPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, FXAA_SHADER_HANDLE, "fxaa.wgsl", Shader::from_wgsl); + embedded_asset!(app, "fxaa.wgsl"); app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); @@ -132,6 +130,7 @@ impl Plugin for FxaaPlugin { pub struct FxaaPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, + shader: Handle, } impl FromWorld for FxaaPipeline { @@ -158,6 +157,7 @@ impl FromWorld for FxaaPipeline { FxaaPipeline { texture_bind_group, sampler, + shader: load_embedded_asset!(render_world, "fxaa.wgsl"), } } } @@ -183,7 +183,7 @@ impl SpecializedRenderPipeline for FxaaPipeline { layout: vec![self.texture_bind_group.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: FXAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: vec![ format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(), format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(), diff --git a/crates/bevy_anti_aliasing/src/lib.rs b/crates/bevy_anti_aliasing/src/lib.rs index be09a2e5b2..12b7982cb5 100644 --- a/crates/bevy_anti_aliasing/src/lib.rs +++ b/crates/bevy_anti_aliasing/src/lib.rs @@ -2,26 +2,25 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] use bevy_app::Plugin; use contrast_adaptive_sharpening::CasPlugin; use fxaa::FxaaPlugin; use smaa::SmaaPlugin; +use taa::TemporalAntiAliasPlugin; pub mod contrast_adaptive_sharpening; -pub mod experimental; pub mod fxaa; pub mod smaa; - -mod taa; +pub mod taa; #[derive(Default)] pub struct AntiAliasingPlugin; impl Plugin for AntiAliasingPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_plugins((FxaaPlugin, CasPlugin, SmaaPlugin)); + app.add_plugins((FxaaPlugin, SmaaPlugin, TemporalAntiAliasPlugin, CasPlugin)); } } diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs index 4259b5e33d..bb082c5a01 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs @@ -32,7 +32,7 @@ use bevy_app::{App, Plugin}; #[cfg(feature = "smaa_luts")] use bevy_asset::load_internal_binary_asset; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Handle}; #[cfg(not(feature = "smaa_luts"))] use bevy_core_pipeline::tonemapping::lut_placeholder; use bevy_core_pipeline::{ @@ -80,8 +80,6 @@ use bevy_render::{ }; use bevy_utils::prelude::default; -/// The handle of the `smaa.wgsl` shader. -const SMAA_SHADER_HANDLE: Handle = weak_handle!("fdd9839f-1ab4-4e0d-88a0-240b67da2ddf"); /// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle = weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0"); @@ -147,6 +145,8 @@ struct SmaaEdgeDetectionPipeline { postprocess_bind_group_layout: BindGroupLayout, /// The bind group layout for data specific to this pass. edge_detection_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } /// The pipeline data for phase 2 of SMAA: blending weight calculation. @@ -155,6 +155,8 @@ struct SmaaBlendingWeightCalculationPipeline { postprocess_bind_group_layout: BindGroupLayout, /// The bind group layout for data specific to this pass. blending_weight_calculation_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } /// The pipeline data for phase 3 of SMAA: neighborhood blending. @@ -163,6 +165,8 @@ struct SmaaNeighborhoodBlendingPipeline { postprocess_bind_group_layout: BindGroupLayout, /// The bind group layout for data specific to this pass. neighborhood_blending_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } /// A unique identifier for a set of SMAA pipelines. @@ -287,7 +291,7 @@ pub struct SmaaSpecializedRenderPipelines { impl Plugin for SmaaPlugin { fn build(&self, app: &mut App) { // Load the shader. - load_internal_asset!(app, SMAA_SHADER_HANDLE, "smaa.wgsl", Shader::from_wgsl); + embedded_asset!(app, "smaa.wgsl"); // Load the two lookup textures. These are compressed textures in KTX2 // format. @@ -431,18 +435,23 @@ impl FromWorld for SmaaPipelines { ), ); + let shader = load_embedded_asset!(world, "smaa.wgsl"); + SmaaPipelines { edge_detection: SmaaEdgeDetectionPipeline { postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), edge_detection_bind_group_layout, + shader: shader.clone(), }, blending_weight_calculation: SmaaBlendingWeightCalculationPipeline { postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), blending_weight_calculation_bind_group_layout, + shader: shader.clone(), }, neighborhood_blending: SmaaNeighborhoodBlendingPipeline { postprocess_bind_group_layout, neighborhood_blending_bind_group_layout, + shader, }, } } @@ -472,13 +481,13 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline { self.edge_detection_bind_group_layout.clone(), ], vertex: VertexState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), entry_point: "edge_detection_vertex_main".into(), buffers: vec![], }, fragment: Some(FragmentState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "luma_edge_detection_fragment_main".into(), targets: vec![Some(ColorTargetState { @@ -532,13 +541,13 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline { self.blending_weight_calculation_bind_group_layout.clone(), ], vertex: VertexState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), entry_point: "blending_weight_calculation_vertex_main".into(), buffers: vec![], }, fragment: Some(FragmentState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "blending_weight_calculation_fragment_main".into(), targets: vec![Some(ColorTargetState { @@ -580,13 +589,13 @@ impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline { self.neighborhood_blending_bind_group_layout.clone(), ], vertex: VertexState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), entry_point: "neighborhood_blending_vertex_main".into(), buffers: vec![], }, fragment: Some(FragmentState { - shader: SMAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "neighborhood_blending_fragment_main".into(), targets: vec![Some(ColorTargetState { @@ -838,7 +847,7 @@ impl ViewNode for SmaaNode { view_smaa_uniform_offset, smaa_textures, view_smaa_bind_groups, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs index dc12d34423..0f706146b1 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_aliasing/src/taa/mod.rs @@ -1,5 +1,5 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, fullscreen_vertex_shader::fullscreen_shader_vertex_state, @@ -40,8 +40,6 @@ use bevy_render::{ }; use tracing::warn; -const TAA_SHADER_HANDLE: Handle = weak_handle!("fea20d50-86b6-4069-aa32-374346aec00c"); - /// Plugin for temporal anti-aliasing. /// /// See [`TemporalAntiAliasing`] for more details. @@ -49,7 +47,7 @@ pub struct TemporalAntiAliasPlugin; impl Plugin for TemporalAntiAliasPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, TAA_SHADER_HANDLE, "taa.wgsl", Shader::from_wgsl); + embedded_asset!(app, "taa.wgsl"); app.register_type::(); @@ -64,7 +62,7 @@ impl Plugin for TemporalAntiAliasPlugin { .add_systems( Render, ( - prepare_taa_jitter_and_mip_bias.in_set(RenderSystems::ManageViews), + prepare_taa_jitter.in_set(RenderSystems::ManageViews), prepare_taa_pipelines.in_set(RenderSystems::Prepare), prepare_taa_history_textures.in_set(RenderSystems::PrepareResources), ), @@ -115,7 +113,6 @@ impl Plugin for TemporalAntiAliasPlugin { /// /// # Usage Notes /// -/// The [`TemporalAntiAliasPlugin`] must be added to your app. /// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`]. /// /// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`]. @@ -128,11 +125,9 @@ impl Plugin for TemporalAntiAliasPlugin { /// /// 1. Write particle motion vectors to the motion vectors prepass texture /// 2. Render particles after TAA -/// -/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component. #[derive(Component, Reflect, Clone)] #[reflect(Component, Default, Clone)] -#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)] +#[require(TemporalJitter, MipBias, DepthPrepass, MotionVectorPrepass)] #[doc(alias = "Taa")] pub struct TemporalAntiAliasing { /// Set to true to delete the saved temporal history (past frames). @@ -243,6 +238,7 @@ struct TaaPipeline { taa_bind_group_layout: BindGroupLayout, nearest_sampler: Sampler, linear_sampler: Sampler, + shader: Handle, } impl FromWorld for TaaPipeline { @@ -287,6 +283,7 @@ impl FromWorld for TaaPipeline { taa_bind_group_layout, nearest_sampler, linear_sampler, + shader: load_embedded_asset!(world, "taa.wgsl"), } } } @@ -319,7 +316,7 @@ impl SpecializedRenderPipeline for TaaPipeline { layout: vec![self.taa_bind_group_layout.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: TAA_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "taa".into(), targets: vec![ @@ -345,16 +342,11 @@ impl SpecializedRenderPipeline for TaaPipeline { } fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut) { - let mut cameras_3d = main_world.query_filtered::<( + let mut cameras_3d = main_world.query::<( RenderEntity, &Camera, &Projection, - &mut TemporalAntiAliasing, - ), ( - With, - With, - With, - With, + Option<&mut TemporalAntiAliasing>, )>(); for (entity, camera, camera_projection, mut taa_settings) in @@ -364,14 +356,12 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut(); @@ -379,13 +369,22 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut, - mut query: Query<(Entity, &mut TemporalJitter, Option<&MipBias>), With>, - mut commands: Commands, + mut query: Query< + &mut TemporalJitter, + ( + With, + With, + With, + With, + With, + ), + >, ) { - // Halton sequence (2, 3) - 0.5, skipping i = 0 + // Halton sequence (2, 3) - 0.5 let halton_sequence = [ + vec2(0.0, 0.0), vec2(0.0, -0.16666666), vec2(-0.25, 0.16666669), vec2(0.25, -0.3888889), @@ -393,17 +392,12 @@ fn prepare_taa_jitter_and_mip_bias( vec2(0.125, 0.2777778), vec2(-0.125, -0.2777778), vec2(0.375, 0.055555582), - vec2(-0.4375, 0.3888889), ]; let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()]; - for (entity, mut jitter, mip_bias) in &mut query { + for mut jitter in &mut query { jitter.offset = offset; - - if mip_bias.is_none() { - commands.entity(entity).insert(MipBias(-1.0)); - } } } diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index f46db94db3..a0c5222b0b 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_app" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides core App functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -47,7 +47,6 @@ std = [ "bevy_ecs/std", "dep:ctrlc", "downcast-rs/std", - "bevy_utils/std", "bevy_tasks/std", "bevy_platform/std", ] @@ -72,16 +71,20 @@ web = [ "dep:console_error_panic_hook", ] +hotpatching = [ + "bevy_ecs/hotpatching", + "dep:dioxus-devtools", + "dep:crossbeam-channel", +] + [dependencies] # bevy -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ - "alloc", -] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other downcast-rs = { version = "2", default-features = false } @@ -90,8 +93,10 @@ variadics_please = "1.1" tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } cfg-if = "1.0.0" +dioxus-devtools = { version = "0.7.0-alpha.1", optional = true } +crossbeam-channel = { version = "0.5.0", optional = true } -[target.'cfg(any(unix, windows))'.dependencies] +[target.'cfg(any(all(unix, not(target_os = "horizon")), windows))'.dependencies] ctrlc = { version = "3.4.4", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 60431ca479..05f3de27b1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -10,6 +10,7 @@ use alloc::{ pub use bevy_derive::AppLabel; use bevy_ecs::{ component::RequiredComponentsError, + error::{DefaultErrorHandler, ErrorHandler}, event::{event_update_system, EventCursor}, intern::Interned, prelude::*, @@ -85,6 +86,7 @@ pub struct App { /// [`WinitPlugin`]: https://docs.rs/bevy/latest/bevy/winit/struct.WinitPlugin.html /// [`ScheduleRunnerPlugin`]: https://docs.rs/bevy/latest/bevy/app/struct.ScheduleRunnerPlugin.html pub(crate) runner: RunnerFn, + default_error_handler: Option, } impl Debug for App { @@ -104,10 +106,13 @@ impl Default for App { #[cfg(feature = "bevy_reflect")] { + use bevy_ecs::observer::ObservedBy; + app.init_resource::(); app.register_type::(); app.register_type::(); app.register_type::(); + app.register_type::(); } #[cfg(feature = "reflect_functions")] @@ -143,6 +148,7 @@ impl App { sub_apps: HashMap::default(), }, runner: Box::new(run_once), + default_error_handler: None, } } @@ -338,7 +344,7 @@ impl App { self } - /// Initializes `T` event handling by inserting an event queue resource ([`Events::`]) + /// Initializes [`BufferedEvent`] handling for `T` by inserting an event queue resource ([`Events::`]) /// and scheduling an [`event_update_system`] in [`First`]. /// /// See [`Events`] for information on how to define events. @@ -349,7 +355,7 @@ impl App { /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # - /// # #[derive(Event)] + /// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # let mut app = App::new(); /// # @@ -357,7 +363,7 @@ impl App { /// ``` pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { self.main_mut().add_event::(); self @@ -1115,7 +1121,12 @@ impl App { } /// Inserts a [`SubApp`] with the given label. - pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) { + pub fn insert_sub_app(&mut self, label: impl AppLabel, mut sub_app: SubApp) { + if let Some(handler) = self.default_error_handler { + sub_app + .world_mut() + .get_resource_or_insert_with(|| DefaultErrorHandler(handler)); + } self.sub_apps.sub_apps.insert(label.intern(), sub_app); } @@ -1298,6 +1309,8 @@ impl App { /// Spawns an [`Observer`] entity, which will watch for and respond to the given event. /// + /// `observer` can be any system whose first parameter is [`On`]. + /// /// # Examples /// /// ```rust @@ -1312,14 +1325,14 @@ impl App { /// # friends_allowed: bool, /// # }; /// # - /// # #[derive(Event)] + /// # #[derive(Event, EntityEvent)] /// # struct Invite; /// # /// # #[derive(Component)] /// # struct Friend; /// # - /// // An observer system can be any system where the first parameter is a trigger - /// app.add_observer(|trigger: Trigger, friends: Query>, mut commands: Commands| { + /// + /// app.add_observer(|trigger: On, friends: Query>, mut commands: Commands| { /// if trigger.event().friends_allowed { /// for friend in friends.iter() { /// commands.trigger_targets(Invite, friend); @@ -1334,6 +1347,49 @@ impl App { self.world_mut().add_observer(observer); self } + + /// Gets the error handler to set for new supapps. + /// + /// Note that the error handler of existing subapps may differ. + pub fn get_error_handler(&self) -> Option { + self.default_error_handler + } + + /// Set the [default error handler] for the all subapps (including the main one and future ones) + /// that do not have one. + /// + /// May only be called once and should be set by the application, not by libraries. + /// + /// The handler will be called when an error is produced and not otherwise handled. + /// + /// # Panics + /// Panics if called multiple times. + /// + /// # Example + /// ``` + /// # use bevy_app::*; + /// # use bevy_ecs::error::warn; + /// # fn MyPlugins(_: &mut App) {} + /// App::new() + /// .set_error_handler(warn) + /// .add_plugins(MyPlugins) + /// .run(); + /// ``` + /// + /// [default error handler]: bevy_ecs::error::DefaultErrorHandler + pub fn set_error_handler(&mut self, handler: ErrorHandler) -> &mut Self { + assert!( + self.default_error_handler.is_none(), + "`set_error_handler` called multiple times on same `App`" + ); + self.default_error_handler = Some(handler); + for sub_app in self.sub_apps.iter_mut() { + sub_app + .world_mut() + .get_resource_or_insert_with(|| DefaultErrorHandler(handler)); + } + self + } } type RunnerFn = Box AppExit>; @@ -1351,7 +1407,7 @@ fn run_once(mut app: App) -> AppExit { app.should_exit().unwrap_or(AppExit::Success) } -/// An event that indicates the [`App`] should exit. If one or more of these are present at the end of an update, +/// A [`BufferedEvent`] that indicates the [`App`] should exit. If one or more of these are present at the end of an update, /// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller. /// /// This event can be used to detect when an exit is requested. Make sure that systems listening @@ -1361,7 +1417,7 @@ fn run_once(mut app: App) -> AppExit { /// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns /// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#)) /// we only allow error codes between 1 and [255](u8::MAX). -#[derive(Event, Debug, Clone, Default, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Default, PartialEq, Eq)] pub enum AppExit { /// [`App`] exited without any problems. #[default] @@ -1429,9 +1485,9 @@ mod tests { change_detection::{DetectChanges, ResMut}, component::Component, entity::Entity, - event::{Event, EventWriter, Events}, + event::{BufferedEvent, Event, EventWriter, Events}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, resource::Resource, schedule::{IntoScheduleConfigs, ScheduleLabel}, system::{Commands, Query}, @@ -1526,7 +1582,7 @@ mod tests { app.add_systems(EnterMainMenu, (foo, bar)); app.world_mut().run_schedule(EnterMainMenu); - assert_eq!(app.world().entities().len(), 2); + assert_eq!(app.world().entity_count(), 2); } #[test] @@ -1795,7 +1851,7 @@ mod tests { } #[test] fn events_should_be_updated_once_per_update() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut app = App::new(); diff --git a/crates/bevy_app/src/hotpatch.rs b/crates/bevy_app/src/hotpatch.rs new file mode 100644 index 0000000000..1f9da40730 --- /dev/null +++ b/crates/bevy_app/src/hotpatch.rs @@ -0,0 +1,42 @@ +//! Utilities for hotpatching code. +extern crate alloc; + +use alloc::sync::Arc; + +use bevy_ecs::{event::EventWriter, HotPatched}; +#[cfg(not(target_family = "wasm"))] +use dioxus_devtools::connect_subsecond; +use dioxus_devtools::subsecond; + +pub use dioxus_devtools::subsecond::{call, HotFunction}; + +use crate::{Last, Plugin}; + +/// Plugin connecting to Dioxus CLI to enable hot patching. +#[derive(Default)] +pub struct HotPatchPlugin; + +impl Plugin for HotPatchPlugin { + fn build(&self, app: &mut crate::App) { + let (sender, receiver) = crossbeam_channel::bounded::(1); + + // Connects to the dioxus CLI that will handle rebuilds + // This will open a connection to the dioxus CLI to receive updated jump tables + // Sends a `HotPatched` message through the channel when the jump table is updated + #[cfg(not(target_family = "wasm"))] + connect_subsecond(); + subsecond::register_handler(Arc::new(move || { + sender.send(HotPatched).unwrap(); + })); + + // Adds a system that will read the channel for new `HotPatched`, and forward them as event to the ECS + app.add_event::().add_systems( + Last, + move |mut events: EventWriter| { + if receiver.try_recv().is_ok() { + events.write_default(); + } + }, + ); + } +} diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 743806df71..188ba957f6 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -8,8 +8,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -28,21 +28,26 @@ mod main_schedule; mod panic_handler; mod plugin; mod plugin_group; +mod propagate; mod schedule_runner; mod sub_app; mod task_pool_plugin; -#[cfg(all(any(unix, windows), feature = "std"))] +#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))] mod terminal_ctrl_c_handler; +#[cfg(feature = "hotpatching")] +pub mod hotpatch; + pub use app::*; pub use main_schedule::*; pub use panic_handler::*; pub use plugin::*; pub use plugin_group::*; +pub use propagate::*; pub use schedule_runner::*; pub use sub_app::*; pub use task_pool_plugin::*; -#[cfg(all(any(unix, windows), feature = "std"))] +#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))] pub use terminal_ctrl_c_handler::*; /// The app prelude. diff --git a/crates/bevy_app/src/panic_handler.rs b/crates/bevy_app/src/panic_handler.rs index 1021a3dc2e..c35d2333bf 100644 --- a/crates/bevy_app/src/panic_handler.rs +++ b/crates/bevy_app/src/panic_handler.rs @@ -1,4 +1,4 @@ -//! This module provides panic handlers for [Bevy](https://bevyengine.org) +//! This module provides panic handlers for [Bevy](https://bevy.org) //! apps, and automatically configures platform specifics (i.e. Wasm or Android). //! //! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`. diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs new file mode 100644 index 0000000000..c6ac5139b9 --- /dev/null +++ b/crates/bevy_app/src/propagate.rs @@ -0,0 +1,552 @@ +use alloc::vec::Vec; +use core::marker::PhantomData; + +use crate::{App, Plugin, Update}; +use bevy_ecs::{ + component::Component, + entity::Entity, + hierarchy::ChildOf, + lifecycle::RemovedComponents, + query::{Changed, Or, QueryFilter, With, Without}, + relationship::{Relationship, RelationshipTarget}, + schedule::{IntoScheduleConfigs, SystemSet}, + system::{Commands, Local, Query}, +}; + +/// Plugin to automatically propagate a component value to all direct and transient relationship +/// targets (e.g. [`bevy_ecs::hierarchy::Children`]) of entities with a [`Propagate`] component. +/// +/// The plugin Will maintain the target component over hierarchy changes, adding or removing +/// `C` when a relationship `R` (e.g. [`ChildOf`]) is added to or removed from a +/// relationship tree with a [`Propagate`] source, or if the [`Propagate`] component +/// is added, changed or removed. +/// +/// Optionally you can include a query filter `F` to restrict the entities that are updated. +/// Note that the filter is not rechecked dynamically: changes to the filter state will not be +/// picked up until the [`Propagate`] component is touched, or the hierarchy is changed. +/// All members of the tree between source and target must match the filter for propagation +/// to reach a given target. +/// Individual entities can be skipped or terminate the propagation with the [`PropagateOver`] +/// and [`PropagateStop`] components. +pub struct HierarchyPropagatePlugin< + C: Component + Clone + PartialEq, + F: QueryFilter = (), + R: Relationship = ChildOf, +>(PhantomData (C, F, R)>); + +/// Causes the inner component to be added to this entity and all direct and transient relationship +/// targets. A target with a [`Propagate`] component of its own will override propagation from +/// that point in the tree. +#[derive(Component, Clone, PartialEq)] +pub struct Propagate(pub C); + +/// Stops the output component being added to this entity. +/// Relationship targets will still inherit the component from this entity or its parents. +#[derive(Component)] +pub struct PropagateOver(PhantomData C>); + +/// Stops the propagation at this entity. Children will not inherit the component. +#[derive(Component)] +pub struct PropagateStop(PhantomData C>); + +/// The set in which propagation systems are added. You can schedule your logic relative to this set. +#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)] +pub struct PropagateSet { + _p: PhantomData C>, +} + +/// Internal struct for managing propagation +#[derive(Component, Clone, PartialEq)] +pub struct Inherited(pub C); + +impl Default + for HierarchyPropagatePlugin +{ + fn default() -> Self { + Self(Default::default()) + } +} + +impl Default for PropagateOver { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Default for PropagateStop { + fn default() -> Self { + Self(Default::default()) + } +} + +impl core::fmt::Debug for PropagateSet { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("PropagateSet") + .field("_p", &self._p) + .finish() + } +} + +impl Eq for PropagateSet {} + +impl core::hash::Hash for PropagateSet { + fn hash(&self, state: &mut H) { + self._p.hash(state); + } +} + +impl Default for PropagateSet { + fn default() -> Self { + Self { + _p: Default::default(), + } + } +} + +impl Plugin + for HierarchyPropagatePlugin +{ + fn build(&self, app: &mut App) { + app.add_systems( + Update, + ( + update_source::, + update_stopped::, + update_reparented::, + propagate_inherited::, + propagate_output::, + ) + .chain() + .in_set(PropagateSet::::default()), + ); + } +} + +/// add/remove `Inherited::` and `C` for entities with a direct `Propagate::` +pub fn update_source( + mut commands: Commands, + changed: Query< + (Entity, &Propagate), + ( + Or<(Changed>, Without>)>, + Without>, + ), + >, + mut removed: RemovedComponents>, +) { + for (entity, source) in &changed { + commands + .entity(entity) + .try_insert(Inherited(source.0.clone())); + } + + for removed in removed.read() { + if let Ok(mut commands) = commands.get_entity(removed) { + commands.remove::<(Inherited, C)>(); + } + } +} + +/// remove `Inherited::` and `C` for entities with a `PropagateStop::` +pub fn update_stopped( + mut commands: Commands, + q: Query>, With>, F)>, +) { + for entity in q.iter() { + let mut cmds = commands.entity(entity); + cmds.remove::<(Inherited, C)>(); + } +} + +/// add/remove `Inherited::` and `C` for entities which have changed relationship +pub fn update_reparented( + mut commands: Commands, + moved: Query< + (Entity, &R, Option<&Inherited>), + ( + Changed, + Without>, + Without>, + F, + ), + >, + relations: Query<&Inherited>, + orphaned: Query>, Without>, Without, F)>, +) { + for (entity, relation, maybe_inherited) in &moved { + if let Ok(inherited) = relations.get(relation.get()) { + commands.entity(entity).try_insert(inherited.clone()); + } else if maybe_inherited.is_some() { + commands.entity(entity).remove::<(Inherited, C)>(); + } + } + + for orphan in &orphaned { + commands.entity(orphan).remove::<(Inherited, C)>(); + } +} + +/// add/remove `Inherited::` for targets of entities with modified `Inherited::` +pub fn propagate_inherited( + mut commands: Commands, + changed: Query< + (&Inherited, &R::RelationshipTarget), + (Changed>, Without>, F), + >, + recurse: Query< + (Option<&R::RelationshipTarget>, Option<&Inherited>), + (Without>, Without>, F), + >, + mut removed: RemovedComponents>, + mut to_process: Local>)>>, +) { + // gather changed + for (inherited, targets) in &changed { + to_process.extend( + targets + .iter() + .map(|target| (target, Some(inherited.clone()))), + ); + } + + // and removed + for entity in removed.read() { + if let Ok((Some(targets), _)) = recurse.get(entity) { + to_process.extend(targets.iter().map(|target| (target, None))); + } + } + + // propagate + while let Some((entity, maybe_inherited)) = (*to_process).pop() { + let Ok((maybe_targets, maybe_current)) = recurse.get(entity) else { + continue; + }; + + if maybe_current == maybe_inherited.as_ref() { + continue; + } + + if let Some(targets) = maybe_targets { + to_process.extend( + targets + .iter() + .map(|target| (target, maybe_inherited.clone())), + ); + } + + if let Some(inherited) = maybe_inherited { + commands.entity(entity).try_insert(inherited.clone()); + } else { + commands.entity(entity).remove::<(Inherited, C)>(); + } + } +} + +/// add `C` to entities with `Inherited::` +pub fn propagate_output( + mut commands: Commands, + changed: Query< + (Entity, &Inherited, Option<&C>), + (Changed>, Without>, F), + >, +) { + for (entity, inherited, maybe_current) in &changed { + if maybe_current.is_some_and(|c| &inherited.0 == c) { + continue; + } + + commands.entity(entity).try_insert(inherited.0.clone()); + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::schedule::Schedule; + + use crate::{App, Update}; + + use super::*; + + #[derive(Component, Clone, PartialEq, Debug)] + struct TestValue(u32); + + #[test] + fn test_simple_propagate() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let intermediate = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(intermediate)) + .id(); + + app.update(); + + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + } + + #[test] + fn test_reparented() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + } + + #[test] + fn test_reparented_with_prior() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator_a)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(1)) + ); + app.world_mut() + .commands() + .entity(propagatee) + .insert(ChildOf(propagator_b)); + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(2)) + ); + } + + #[test] + fn test_remove_orphan() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + app.world_mut() + .commands() + .entity(propagatee) + .remove::(); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + } + + #[test] + fn test_remove_propagated() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + app.world_mut() + .commands() + .entity(propagator) + .remove::>(); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + } + + #[test] + fn test_propagate_over() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_over = app + .world_mut() + .spawn(TestValue(2)) + .insert(ChildOf(propagator)) + .id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagate_over)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(1)) + ); + } + + #[test] + fn test_propagate_stop() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_stop = app + .world_mut() + .spawn(PropagateStop::::default()) + .insert(ChildOf(propagator)) + .id(); + let no_propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagate_stop)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), no_propagatee) + .is_err()); + } + + #[test] + fn test_intermediate_override() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let intermediate = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(intermediate)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(1)) + ); + + app.world_mut() + .entity_mut(intermediate) + .insert(Propagate(TestValue(2))); + app.update(); + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee), + Ok(&TestValue(2)) + ); + } + + #[test] + fn test_filter() { + #[derive(Component)] + struct Marker; + + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::>::default()); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app + .world_mut() + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + + // NOTE: changes to the filter condition are not rechecked + app.world_mut().entity_mut(propagator).insert(Marker); + app.world_mut().entity_mut(propagatee).insert(Marker); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_err()); + + app.world_mut() + .entity_mut(propagator) + .insert(Propagate(TestValue(1))); + app.update(); + assert!(app + .world_mut() + .query::<&TestValue>() + .get(app.world(), propagatee) + .is_ok()); + } +} diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index c340b80654..56d6b43d38 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -338,7 +338,7 @@ impl SubApp { /// See [`App::add_event`]. pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { if !self.world.contains_resource::>() { EventRegistry::register_event::(self.world_mut()); diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 07a45a3f6d..0835b3b49a 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_asset" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides asset functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -19,19 +19,19 @@ watch = [] trace = [] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_asset_macros = { path = "macros", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_asset_macros = { path = "macros", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "uuid", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "async_executor", ] } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -54,7 +54,7 @@ parking_lot = { version = "0.12", default-features = false, features = [ "arc_lock", "send_guard", ] } -ron = { version = "0.8", default-features = false } +ron = { version = "0.10", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } @@ -65,7 +65,7 @@ uuid = { version = "1.13.1", default-features = false, features = [ tracing = { version = "0.1", default-features = false } [target.'cfg(target_os = "android")'.dependencies] -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. @@ -78,13 +78,13 @@ web-sys = { version = "0.3", features = [ wasm-bindgen-futures = "0.4" js-sys = "0.3" uuid = { version = "1.13.1", default-features = false, features = ["js"] } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml index 43562ae806..0b525b3c1d 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_asset_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_asset" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_asset/macros/src/lib.rs b/crates/bevy_asset/macros/src/lib.rs index 443bd09ab9..a7ea87b752 100644 --- a/crates/bevy_asset/macros/src/lib.rs +++ b/crates/bevy_asset/macros/src/lib.rs @@ -1,6 +1,7 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +//! Macros for deriving asset traits. + use bevy_macro_utils::BevyManifest; use proc_macro::{Span, TokenStream}; use quote::{format_ident, quote}; @@ -12,6 +13,7 @@ pub(crate) fn bevy_asset_path() -> Path { const DEPENDENCY_ATTRIBUTE: &str = "dependency"; +/// Implement the `Asset` trait. #[proc_macro_derive(Asset, attributes(dependency))] pub fn derive_asset(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -30,6 +32,7 @@ pub fn derive_asset(input: TokenStream) -> TokenStream { }) } +/// Implement the `VisitAssetDependencies` trait. #[proc_macro_derive(VisitAssetDependencies, attributes(dependency))] pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index 10f298c968..b43a8625e7 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -106,7 +106,7 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// - Removed assets are not detected. /// /// The list of changed assets only gets updated in the [`AssetEventSystems`] system set, -/// which runs in `Last`. Therefore, `AssetChanged` will only pick up asset changes in schedules +/// which runs in `PostUpdate`. Therefore, `AssetChanged` will only pick up asset changes in schedules /// following [`AssetEventSystems`] or the next frame. Consider adding the system in the `Last` schedule /// after [`AssetEventSystems`] if you need to react without frame delay to asset changes. /// @@ -158,9 +158,9 @@ unsafe impl WorldQuery for AssetChanged { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -201,9 +201,9 @@ unsafe impl WorldQuery for AssetChanged { const IS_DENSE: bool = <&A>::IS_DENSE; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -215,7 +215,11 @@ unsafe impl WorldQuery for AssetChanged { } } - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + table: &'w Table, + ) { if let Some(inner) = &mut fetch.inner { // SAFETY: We delegate to the inner `set_table` for `A` unsafe { @@ -265,6 +269,7 @@ unsafe impl QueryFilter for AssetChanged { #[inline] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -272,7 +277,7 @@ unsafe impl QueryFilter for AssetChanged { fetch.inner.as_mut().is_some_and(|inner| { // SAFETY: We delegate to the inner `fetch` for `A` unsafe { - let handle = <&A>::fetch(inner, entity, table_row); + let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row); fetch.check.has_changed(handle) } }) diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index 9fa8eb4381..6e5b488ee0 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -437,6 +437,18 @@ impl Assets { result } + /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists. + /// + /// This is the same as [`Assets::get_mut`] except it doesn't emit [`AssetEvent::Modified`]. + #[inline] + pub fn get_mut_untracked(&mut self, id: impl Into>) -> Option<&mut A> { + let id: AssetId = id.into(); + match id { + AssetId::Index { index, .. } => self.dense_storage.get_mut(index), + AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), + } + } + /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. pub fn remove(&mut self, id: impl Into>) -> Option { @@ -450,6 +462,8 @@ impl Assets { /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. This skips emitting [`AssetEvent::Removed`]. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. + /// + /// This is the same as [`Assets::remove`] except it doesn't emit [`AssetEvent::Removed`]. pub fn remove_untracked(&mut self, id: impl Into>) -> Option { let id: AssetId = id.into(); self.duplicate_handles.remove(&id); diff --git a/crates/bevy_asset/src/direct_access_ext.rs b/crates/bevy_asset/src/direct_access_ext.rs index 792d523a30..e7e5b993de 100644 --- a/crates/bevy_asset/src/direct_access_ext.rs +++ b/crates/bevy_asset/src/direct_access_ext.rs @@ -20,6 +20,7 @@ pub trait DirectAssetAccessExt { settings: impl Fn(&mut S) + Send + Sync + 'static, ) -> Handle; } + impl DirectAssetAccessExt for World { /// Insert an asset similarly to [`Assets::add`]. /// diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index 087cb44b5a..42de19fe44 100644 --- a/crates/bevy_asset/src/event.rs +++ b/crates/bevy_asset/src/event.rs @@ -1,12 +1,12 @@ use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_reflect::Reflect; use core::fmt::Debug; -/// An event emitted when a specific [`Asset`] fails to load. +/// A [`BufferedEvent`] emitted when a specific [`Asset`] fails to load. /// /// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. -#[derive(Event, Clone, Debug)] +#[derive(Event, BufferedEvent, Clone, Debug)] pub struct AssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: AssetId, @@ -24,7 +24,7 @@ impl AssetLoadFailedEvent { } /// An untyped version of [`AssetLoadFailedEvent`]. -#[derive(Event, Clone, Debug)] +#[derive(Event, BufferedEvent, Clone, Debug)] pub struct UntypedAssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: UntypedAssetId, @@ -44,9 +44,9 @@ impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent { } } -/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. +/// [`BufferedEvent`]s that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. #[expect(missing_docs, reason = "Documenting the id fields is unhelpful.")] -#[derive(Event, Reflect)] +#[derive(Event, BufferedEvent, Reflect)] pub enum AssetEvent { /// Emitted whenever an [`Asset`] is added. Added { id: AssetId }, diff --git a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs index f7fb56be74..06a0791a50 100644 --- a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -56,6 +56,7 @@ pub(crate) struct EmbeddedEventHandler { dir: Dir, last_event: Option, } + impl FilesystemEventHandler for EmbeddedEventHandler { fn begin(&mut self) { self.last_event = None; diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index e63d415342..c49d55ca4a 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -8,8 +8,10 @@ use crate::io::{ memory::{Dir, MemoryAssetReader, Value}, AssetSource, AssetSourceBuilders, }; +use crate::AssetServer; use alloc::boxed::Box; -use bevy_ecs::resource::Resource; +use bevy_app::App; +use bevy_ecs::{resource::Resource, world::World}; use std::path::{Path, PathBuf}; #[cfg(feature = "embedded_watcher")] @@ -132,6 +134,74 @@ impl EmbeddedAssetRegistry { } } +/// Trait for the [`load_embedded_asset!`] macro, to access [`AssetServer`] +/// from arbitrary things. +/// +/// [`load_embedded_asset!`]: crate::load_embedded_asset +pub trait GetAssetServer { + fn get_asset_server(&self) -> &AssetServer; +} + +impl GetAssetServer for App { + fn get_asset_server(&self) -> &AssetServer { + self.world().get_asset_server() + } +} + +impl GetAssetServer for World { + fn get_asset_server(&self) -> &AssetServer { + self.resource() + } +} + +impl GetAssetServer for AssetServer { + fn get_asset_server(&self) -> &AssetServer { + self + } +} + +/// Load an [embedded asset](crate::embedded_asset). +/// +/// This is useful if the embedded asset in question is not publicly exposed, but +/// you need to use it internally. +/// +/// # Syntax +/// +/// This macro takes two arguments and an optional third one: +/// 1. The asset source. It may be `AssetServer`, `World` or `App`. +/// 2. The path to the asset to embed, as a string literal. +/// 3. Optionally, a closure of the same type as in [`AssetServer::load_with_settings`]. +/// Consider explicitly typing the closure argument in case of type error. +/// +/// # Usage +/// +/// The advantage compared to using directly [`AssetServer::load`] is: +/// - This also accepts [`World`] and [`App`] arguments. +/// - This uses the exact same path as `embedded_asset!`, so you can keep it +/// consistent. +/// +/// As a rule of thumb: +/// - If the asset in used in the same module as it is declared using `embedded_asset!`, +/// use this macro. +/// - Otherwise, use `AssetServer::load`. +#[macro_export] +macro_rules! load_embedded_asset { + (@get: $path: literal, $provider: expr) => {{ + let path = $crate::embedded_path!($path); + let path = $crate::AssetPath::from_path_buf(path).with_source("embedded"); + let asset_server = $crate::io::embedded::GetAssetServer::get_asset_server($provider); + (path, asset_server) + }}; + ($provider: expr, $path: literal, $settings: expr) => {{ + let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider); + asset_server.load_with_settings(path, $settings) + }}; + ($provider: expr, $path: literal) => {{ + let (path, asset_server) = $crate::load_embedded_asset!(@get: $path, $provider); + asset_server.load(path) + }}; +} + /// Returns the [`Path`] for a given `embedded` asset. /// This is used internally by [`embedded_asset`] and can be used to get a [`Path`] /// that matches the [`AssetPath`](crate::AssetPath) used by that asset. @@ -140,7 +210,7 @@ impl EmbeddedAssetRegistry { #[macro_export] macro_rules! embedded_path { ($path_str: expr) => {{ - embedded_path!("src", $path_str) + $crate::embedded_path!("src", $path_str) }}; ($source_path: expr, $path_str: expr) => {{ @@ -192,7 +262,7 @@ pub fn _embedded_asset_path( /// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary /// and registering those bytes with the `embedded` [`AssetSource`]. /// -/// This accepts the current [`App`](bevy_app::App) as the first parameter and a path `&str` (relative to the current file) as the second. +/// This accepts the current [`App`] as the first parameter and a path `&str` (relative to the current file) as the second. /// /// By default this will generate an [`AssetPath`] using the following rules: /// @@ -217,14 +287,19 @@ pub fn _embedded_asset_path( /// /// `embedded_asset!(app, "rock.wgsl")` /// -/// `rock.wgsl` can now be loaded by the [`AssetServer`](crate::AssetServer) with the following path: +/// `rock.wgsl` can now be loaded by the [`AssetServer`] as follows: /// /// ```no_run -/// # use bevy_asset::{Asset, AssetServer}; +/// # use bevy_asset::{Asset, AssetServer, load_embedded_asset}; /// # use bevy_reflect::TypePath; /// # let asset_server: AssetServer = panic!(); /// # #[derive(Asset, TypePath)] /// # struct Shader; +/// // If we are loading the shader in the same module we used `embedded_asset!`: +/// let shader = load_embedded_asset!(&asset_server, "rock.wgsl"); +/// # let _: bevy_asset::Handle = shader; +/// +/// // If the goal is to expose the asset **to the end user**: /// let shader = asset_server.load::("embedded://bevy_rock/render/rock.wgsl"); /// ``` /// @@ -258,11 +333,11 @@ pub fn _embedded_asset_path( /// [`embedded_path`]: crate::embedded_path #[macro_export] macro_rules! embedded_asset { - ($app: ident, $path: expr) => {{ + ($app: expr, $path: expr) => {{ $crate::embedded_asset!($app, "src", $path) }}; - ($app: ident, $source_path: expr, $path: expr) => {{ + ($app: expr, $source_path: expr, $path: expr) => {{ let mut embedded = $app .world_mut() .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>(); diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index c2551a40f1..4080e03ecd 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -81,7 +81,10 @@ impl HttpWasmAssetReader { let reader = VecReader::new(bytes); Ok(reader) } - 404 => Err(AssetReaderError::NotFound(path)), + // Some web servers, including itch.io's CDN, return 403 when a requested file isn't present. + // TODO: remove handling of 403 as not found when it's easier to configure + // see https://github.com/bevyengine/bevy/pull/19268#pullrequestreview-2882410105 + 403 | 404 => Err(AssetReaderError::NotFound(path)), status => Err(AssetReaderError::HttpError(status)), } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 5b680eb191..4b29beae79 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -141,8 +141,8 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 8f4863b885..24405f0657 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -12,7 +12,7 @@ use alloc::{ vec::Vec, }; use atomicow::CowArc; -use bevy_ecs::world::World; +use bevy_ecs::{error::BevyError, world::World}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::any::{Any, TypeId}; @@ -34,7 +34,7 @@ pub trait AssetLoader: Send + Sync + 'static { /// The settings type used by this [`AssetLoader`]. type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>; /// The type of [error](`std::error::Error`) which could be encountered by this loader. - type Error: Into>; + type Error: Into; /// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`]. fn load( &self, @@ -58,10 +58,7 @@ pub trait ErasedAssetLoader: Send + Sync + 'static { reader: &'a mut dyn Reader, meta: &'a dyn AssetMetaDyn, load_context: LoadContext<'a>, - ) -> BoxedFuture< - 'a, - Result>, - >; + ) -> BoxedFuture<'a, Result>; /// Returns a list of extensions supported by this asset loader, without the preceding dot. fn extensions(&self) -> &[&str]; @@ -89,10 +86,7 @@ where reader: &'a mut dyn Reader, meta: &'a dyn AssetMetaDyn, mut load_context: LoadContext<'a>, - ) -> BoxedFuture< - 'a, - Result>, - > { + ) -> BoxedFuture<'a, Result> { Box::pin(async move { let settings = meta .loader_settings() @@ -350,7 +344,7 @@ impl<'a> LoadContext<'a> { /// Begins a new labeled asset load. Use the returned [`LoadContext`] to load /// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load. - /// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent + /// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent /// context. /// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add /// the labeled [`LoadContext`] back to the parent context. @@ -366,7 +360,7 @@ impl<'a> LoadContext<'a> { /// # let load_context: LoadContext = panic!(); /// let mut handles = Vec::new(); /// for i in 0..2 { - /// let mut labeled = load_context.begin_labeled_asset(); + /// let labeled = load_context.begin_labeled_asset(); /// handles.push(std::thread::spawn(move || { /// (i.to_string(), labeled.finish(Image::default())) /// })); @@ -391,18 +385,18 @@ impl<'a> LoadContext<'a> { /// [`LoadedAsset`], which is registered under the `label` label. /// /// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the - /// result with [`LoadContext::add_labeled_asset`]. + /// result with [`LoadContext::add_loaded_labeled_asset`]. /// /// See [`AssetPath`] for more on labeled assets. - pub fn labeled_asset_scope( + pub fn labeled_asset_scope( &mut self, label: String, - load: impl FnOnce(&mut LoadContext) -> A, - ) -> Handle { + load: impl FnOnce(&mut LoadContext) -> Result, + ) -> Result, E> { let mut context = self.begin_labeled_asset(); - let asset = load(&mut context); + let asset = load(&mut context)?; let loaded_asset = context.finish(asset); - self.add_loaded_labeled_asset(label, loaded_asset) + Ok(self.add_loaded_labeled_asset(label, loaded_asset)) } /// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label. @@ -416,7 +410,8 @@ impl<'a> LoadContext<'a> { /// /// See [`AssetPath`] for more on labeled assets. pub fn add_labeled_asset(&mut self, label: String, asset: A) -> Handle { - self.labeled_asset_scope(label, |_| asset) + self.labeled_asset_scope(label, |_| Ok::<_, ()>(asset)) + .expect("the closure returns Ok") } /// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context. diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index ad127812dc..3f780e3fb7 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -223,6 +223,16 @@ impl<'a> AssetPath<'a> { Ok((source, path, label)) } + /// Creates a new [`AssetPath`] from a [`PathBuf`]. + #[inline] + pub fn from_path_buf(path_buf: PathBuf) -> AssetPath<'a> { + AssetPath { + path: CowArc::Owned(path_buf.into()), + source: AssetSourceId::Default, + label: None, + } + } + /// Creates a new [`AssetPath`] from a [`Path`]. #[inline] pub fn from_path(path: &'a Path) -> AssetPath<'a> { @@ -480,7 +490,7 @@ impl<'a> AssetPath<'a> { } /// Returns `true` if this [`AssetPath`] points to a file that is - /// outside of it's [`AssetSource`](crate::io::AssetSource) folder. + /// outside of its [`AssetSource`](crate::io::AssetSource) folder. /// /// ## Example /// ``` diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 5c436c1061..a3148cecb7 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -18,16 +18,16 @@ pub struct ReflectAsset { handle_type_id: TypeId, assets_resource_type_id: TypeId, - get: fn(&World, UntypedHandle) -> Option<&dyn Reflect>, + get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>, // SAFETY: // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably // - may only be used to access **at most one** access at once - get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>, + get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>, add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, - insert: fn(&mut World, UntypedHandle, &dyn PartialReflect), + insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect), len: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, - remove: fn(&mut World, UntypedHandle) -> Option>, + remove: fn(&mut World, UntypedAssetId) -> Option>, } impl ReflectAsset { @@ -42,15 +42,19 @@ impl ReflectAsset { } /// Equivalent of [`Assets::get`] - pub fn get<'w>(&self, world: &'w World, handle: UntypedHandle) -> Option<&'w dyn Reflect> { - (self.get)(world, handle) + pub fn get<'w>( + &self, + world: &'w World, + asset_id: impl Into, + ) -> Option<&'w dyn Reflect> { + (self.get)(world, asset_id.into()) } /// Equivalent of [`Assets::get_mut`] pub fn get_mut<'w>( &self, world: &'w mut World, - handle: UntypedHandle, + asset_id: impl Into, ) -> Option<&'w mut dyn Reflect> { // SAFETY: unique world access #[expect( @@ -58,7 +62,7 @@ impl ReflectAsset { reason = "Use of unsafe `Self::get_unchecked_mut()` function." )] unsafe { - (self.get_unchecked_mut)(world.as_unsafe_world_cell(), handle) + (self.get_unchecked_mut)(world.as_unsafe_world_cell(), asset_id.into()) } } @@ -76,8 +80,8 @@ impl ReflectAsset { /// # let handle_1: UntypedHandle = unimplemented!(); /// # let handle_2: UntypedHandle = unimplemented!(); /// let unsafe_world_cell = world.as_unsafe_world_cell(); - /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_1).unwrap() }; - /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_2).unwrap() }; + /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_1).unwrap() }; + /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_2).unwrap() }; /// // ^ not allowed, two mutable references through the same asset resource, even though the /// // handles are distinct /// @@ -96,10 +100,10 @@ impl ReflectAsset { pub unsafe fn get_unchecked_mut<'w>( &self, world: UnsafeWorldCell<'w>, - handle: UntypedHandle, + asset_id: impl Into, ) -> Option<&'w mut dyn Reflect> { // SAFETY: requirements are deferred to the caller - unsafe { (self.get_unchecked_mut)(world, handle) } + unsafe { (self.get_unchecked_mut)(world, asset_id.into()) } } /// Equivalent of [`Assets::add`] @@ -107,13 +111,22 @@ impl ReflectAsset { (self.add)(world, value) } /// Equivalent of [`Assets::insert`] - pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn PartialReflect) { - (self.insert)(world, handle, value); + pub fn insert( + &self, + world: &mut World, + asset_id: impl Into, + value: &dyn PartialReflect, + ) { + (self.insert)(world, asset_id.into(), value); } /// Equivalent of [`Assets::remove`] - pub fn remove(&self, world: &mut World, handle: UntypedHandle) -> Option> { - (self.remove)(world, handle) + pub fn remove( + &self, + world: &mut World, + asset_id: impl Into, + ) -> Option> { + (self.remove)(world, asset_id.into()) } /// Equivalent of [`Assets::len`] @@ -137,17 +150,17 @@ impl FromType for ReflectAsset { ReflectAsset { handle_type_id: TypeId::of::>(), assets_resource_type_id: TypeId::of::>(), - get: |world, handle| { + get: |world, asset_id| { let assets = world.resource::>(); - let asset = assets.get(&handle.typed_debug_checked()); + let asset = assets.get(asset_id.typed_debug_checked()); asset.map(|asset| asset as &dyn Reflect) }, - get_unchecked_mut: |world, handle| { + get_unchecked_mut: |world, asset_id| { // SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets`, // and must ensure to only have at most one reference to it live at all times. #[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")] let assets = unsafe { world.get_resource_mut::>().unwrap().into_inner() }; - let asset = assets.get_mut(&handle.typed_debug_checked()); + let asset = assets.get_mut(asset_id.typed_debug_checked()); asset.map(|asset| asset as &mut dyn Reflect) }, add: |world, value| { @@ -156,11 +169,11 @@ impl FromType for ReflectAsset { .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`"); assets.add(value).untyped() }, - insert: |world, handle, value| { + insert: |world, asset_id, value| { let mut assets = world.resource_mut::>(); let value: A = FromReflect::from_reflect(value) .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`"); - assets.insert(&handle.typed_debug_checked(), value); + assets.insert(asset_id.typed_debug_checked(), value); }, len: |world| { let assets = world.resource::>(); @@ -170,9 +183,9 @@ impl FromType for ReflectAsset { let assets = world.resource::>(); Box::new(assets.ids().map(AssetId::untyped)) }, - remove: |world, handle| { + remove: |world, asset_id| { let mut assets = world.resource_mut::>(); - let value = assets.remove(&handle.typed_debug_checked()); + let value = assets.remove(asset_id.typed_debug_checked()); value.map(|value| Box::new(value) as Box) }, } @@ -200,7 +213,7 @@ impl FromType for ReflectAsset { /// let reflect_asset = type_registry.get_type_data::(reflect_handle.asset_type_id()).unwrap(); /// /// let handle = reflect_handle.downcast_handle_untyped(handle.as_any()).unwrap(); -/// let value = reflect_asset.get(world, handle).unwrap(); +/// let value = reflect_asset.get(world, &handle).unwrap(); /// println!("{value:?}"); /// } /// ``` @@ -210,6 +223,7 @@ pub struct ReflectHandle { downcast_handle_untyped: fn(&dyn Any) -> Option, typed: fn(UntypedHandle) -> Box, } + impl ReflectHandle { /// The [`TypeId`] of the asset pub fn asset_type_id(&self) -> TypeId { @@ -247,7 +261,7 @@ mod tests { use alloc::{string::String, vec::Vec}; use core::any::TypeId; - use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset, UntypedHandle}; + use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset}; use bevy_app::App; use bevy_ecs::reflect::AppTypeRegistry; use bevy_reflect::Reflect; @@ -281,7 +295,7 @@ mod tests { let handle = reflect_asset.add(app.world_mut(), &value); // struct is a reserved keyword, so we can't use it here let strukt = reflect_asset - .get_mut(app.world_mut(), handle) + .get_mut(app.world_mut(), &handle) .unwrap() .reflect_mut() .as_struct() @@ -294,16 +308,12 @@ mod tests { assert_eq!(reflect_asset.len(app.world()), 1); let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); assert_eq!(ids.len(), 1); + let id = ids[0]; - let fetched_handle = UntypedHandle::Weak(ids[0]); - let asset = reflect_asset - .get(app.world(), fetched_handle.clone_weak()) - .unwrap(); + let asset = reflect_asset.get(app.world(), id).unwrap(); assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); - reflect_asset - .remove(app.world_mut(), fetched_handle) - .unwrap(); + reflect_asset.remove(app.world_mut(), id).unwrap(); assert_eq!(reflect_asset.len(app.world()), 0); } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index ff5800474d..e120888616 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -1945,7 +1945,7 @@ pub enum AssetLoadError { pub struct AssetLoaderError { path: AssetPath<'static>, loader_name: &'static str, - error: Arc, + error: Arc, } impl AssetLoaderError { @@ -1953,6 +1953,14 @@ impl AssetLoaderError { pub fn path(&self) -> &AssetPath<'static> { &self.path } + + /// The error the loader reported when attempting to load the asset. + /// + /// If you know the type of the error the asset loader returned, you can use + /// [`BevyError::downcast_ref()`] to get it. + pub fn error(&self) -> &BevyError { + &self.error + } } /// An error that occurs while resolving an asset added by `add_async`. diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index 84060fe26b..2ffa62db9d 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -1,39 +1,45 @@ [package] name = "bevy_audio" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides audio functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } # other +# TODO: Remove `coreaudio-sys` dep below when updating `cpal`. rodio = { version = "0.20", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_os = "android")'.dependencies] cpal = { version = "0.15", optional = true } +[target.'cfg(target_vendor = "apple")'.dependencies] +# NOTE: Explicitly depend on this patch version to fix: +# https://github.com/bevyengine/bevy/issues/18893 +coreaudio-sys = { version = "0.2.17", default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. rodio = { version = "0.20", default-features = false, features = [ "wasm-bindgen", ] } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 349cf6b6a4..8a4a406acc 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -57,6 +57,16 @@ pub struct PlaybackSettings { /// Optional scale factor applied to the positions of this audio source and the listener, /// overriding the default value configured on [`AudioPlugin::default_spatial_scale`](crate::AudioPlugin::default_spatial_scale). pub spatial_scale: Option, + /// The point in time in the audio clip where playback should start. If set to `None`, it will + /// play from the beginning of the clip. + /// + /// If the playback mode is set to `Loop`, each loop will start from this position. + pub start_position: Option, + /// How long the audio should play before stopping. If set, the clip will play for at most + /// the specified duration. If set to `None`, it will play for as long as it can. + /// + /// If the playback mode is set to `Loop`, each loop will last for this duration. + pub duration: Option, } impl Default for PlaybackSettings { @@ -81,6 +91,8 @@ impl PlaybackSettings { muted: false, spatial: false, spatial_scale: None, + start_position: None, + duration: None, }; /// Will play the associated audio source in a loop. @@ -136,6 +148,18 @@ impl PlaybackSettings { self.spatial_scale = Some(spatial_scale); self } + + /// Helper to use a custom playback start position. + pub const fn with_start_position(mut self, start_position: core::time::Duration) -> Self { + self.start_position = Some(start_position); + self + } + + /// Helper to use a custom playback duration. + pub const fn with_duration(mut self, duration: core::time::Duration) -> Self { + self.duration = Some(duration); + self + } } /// Settings for the listener for spatial audio sources. diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 1869fb4755..d02d326501 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -57,6 +57,7 @@ pub struct PlaybackRemoveMarker; pub(crate) struct EarPositions<'w, 's> { pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>, } + impl<'w, 's> EarPositions<'w, 's> { /// Gets a set of transformed ear positions. /// @@ -156,12 +157,49 @@ pub(crate) fn play_queued_audio_system( } }; + let decoder = audio_source.decoder(); + match settings.mode { - PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), + PlaybackMode::Loop => match (settings.start_position, settings.duration) { + // custom start position and duration + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration) + .repeat_infinite(), + ), + + // custom start position + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position).repeat_infinite()); + } + + // custom duration + (None, Some(duration)) => { + sink.append(decoder.take_duration(duration).repeat_infinite()); + } + + // full clip + (None, None) => sink.append(decoder.repeat_infinite()), + }, PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { - sink.append(audio_source.decoder()); + match (settings.start_position, settings.duration) { + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration), + ), + + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position)); + } + + (None, Some(duration)) => sink.append(decoder.take_duration(duration)), + + (None, None) => sink.append(decoder), + } } - }; + } let mut sink = SpatialAudioSink::new(sink); @@ -196,12 +234,49 @@ pub(crate) fn play_queued_audio_system( } }; + let decoder = audio_source.decoder(); + match settings.mode { - PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), + PlaybackMode::Loop => match (settings.start_position, settings.duration) { + // custom start position and duration + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration) + .repeat_infinite(), + ), + + // custom start position + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position).repeat_infinite()); + } + + // custom duration + (None, Some(duration)) => { + sink.append(decoder.take_duration(duration).repeat_infinite()); + } + + // full clip + (None, None) => sink.append(decoder.repeat_infinite()), + }, PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { - sink.append(audio_source.decoder()); + match (settings.start_position, settings.duration) { + (Some(start_position), Some(duration)) => sink.append( + decoder + .skip_duration(start_position) + .take_duration(duration), + ), + + (Some(start_position), None) => { + sink.append(decoder.skip_duration(start_position)); + } + + (None, Some(duration)) => sink.append(decoder.take_duration(duration)), + + (None, None) => sink.append(decoder), + } } - }; + } let mut sink = AudioSink::new(sink); diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index becbf5d1da..e3b5e02569 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Audio support for the game engine Bevy diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index ed51754f86..1e020d1fd8 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -42,6 +42,14 @@ pub trait AudioSinkPlayback { /// No effect if not paused. fn play(&self); + /// Returns the position of the sound that's being played. + /// + /// This takes into account any speedup or delay applied. + /// + /// Example: if you [`set_speed(2.0)`](Self::set_speed) and [`position()`](Self::position) returns *5s*, + /// then the position in the recording is *10s* from its start. + fn position(&self) -> Duration; + /// Attempts to seek to a given position in the current source. /// /// This blocks between 0 and ~5 milliseconds. @@ -181,6 +189,10 @@ impl AudioSinkPlayback for AudioSink { self.sink.play(); } + fn position(&self) -> Duration { + self.sink.get_pos() + } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { self.sink.try_seek(pos) } @@ -281,6 +293,10 @@ impl AudioSinkPlayback for SpatialAudioSink { self.sink.play(); } + fn position(&self) -> Duration { + self.sink.get_pos() + } + fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { self.sink.try_seek(pos) } diff --git a/crates/bevy_audio/src/volume.rs b/crates/bevy_audio/src/volume.rs index b1378ae485..1f1f417594 100644 --- a/crates/bevy_audio/src/volume.rs +++ b/crates/bevy_audio/src/volume.rs @@ -34,7 +34,7 @@ impl GlobalVolume { #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Clone, Debug, PartialEq)] pub enum Volume { - /// Create a new [`Volume`] from the given volume in linear scale. + /// Create a new [`Volume`] from the given volume in the linear scale. /// /// In a linear scale, the value `1.0` represents the "normal" volume, /// meaning the audio is played at its original level. Values greater than @@ -144,7 +144,7 @@ impl Volume { /// Returns the volume in decibels as a float. /// - /// If the volume is silent / off / muted, i.e. it's underlying linear scale + /// If the volume is silent / off / muted, i.e., its underlying linear scale /// is `0.0`, this method returns negative infinity. pub fn to_decibels(&self) -> f32 { match self { @@ -155,57 +155,95 @@ impl Volume { /// The silent volume. Also known as "off" or "muted". pub const SILENT: Self = Volume::Linear(0.0); -} -impl core::ops::Add for Volume { - type Output = Self; - - fn add(self, rhs: Self) -> Self { - use Volume::{Decibels, Linear}; - - match (self, rhs) { - (Linear(a), Linear(b)) => Linear(a + b), - (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( - decibels_to_linear(a) + decibels_to_linear(b), - )), - // {Linear, Decibels} favors the left hand side of the operation by - // first converting the right hand side to the same type as the left - // hand side and then performing the operation. - (Linear(..), Decibels(db)) => self + Linear(decibels_to_linear(db)), - (Decibels(..), Linear(l)) => self + Decibels(linear_to_decibels(l)), - } + /// Increases the volume by the specified percentage. + /// + /// This method works in the linear domain, where a 100% increase + /// means doubling the volume (equivalent to +6.02dB). + /// + /// # Arguments + /// * `percentage` - The percentage to increase (50.0 means 50% increase) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let volume = Volume::Linear(1.0); + /// let increased = volume.increase_by_percentage(100.0); + /// assert_eq!(increased.to_linear(), 2.0); + /// ``` + pub fn increase_by_percentage(&self, percentage: f32) -> Self { + let factor = 1.0 + (percentage / 100.0); + Volume::Linear(self.to_linear() * factor) } -} -impl core::ops::AddAssign for Volume { - fn add_assign(&mut self, rhs: Self) { - *self = *self + rhs; + /// Decreases the volume by the specified percentage. + /// + /// This method works in the linear domain, where a 50% decrease + /// means halving the volume (equivalent to -6.02dB). + /// + /// # Arguments + /// * `percentage` - The percentage to decrease (50.0 means 50% decrease) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let volume = Volume::Linear(1.0); + /// let decreased = volume.decrease_by_percentage(50.0); + /// assert_eq!(decreased.to_linear(), 0.5); + /// ``` + pub fn decrease_by_percentage(&self, percentage: f32) -> Self { + let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0); + Volume::Linear(self.to_linear() * factor) } -} -impl core::ops::Sub for Volume { - type Output = Self; - - fn sub(self, rhs: Self) -> Self { - use Volume::{Decibels, Linear}; - - match (self, rhs) { - (Linear(a), Linear(b)) => Linear(a - b), - (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( - decibels_to_linear(a) - decibels_to_linear(b), - )), - // {Linear, Decibels} favors the left hand side of the operation by - // first converting the right hand side to the same type as the left - // hand side and then performing the operation. - (Linear(..), Decibels(db)) => self - Linear(decibels_to_linear(db)), - (Decibels(..), Linear(l)) => self - Decibels(linear_to_decibels(l)), - } + /// Scales the volume to a specific linear factor relative to the current volume. + /// + /// This is different from `adjust_by_linear` as it sets the volume to be + /// exactly the factor times the original volume, rather than applying + /// the factor to the current volume. + /// + /// # Arguments + /// * `factor` - The scaling factor (2.0 = twice as loud, 0.5 = half as loud) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let volume = Volume::Linear(0.8); + /// let scaled = volume.scale_to_factor(1.25); + /// assert_eq!(scaled.to_linear(), 1.0); + /// ``` + pub fn scale_to_factor(&self, factor: f32) -> Self { + Volume::Linear(self.to_linear() * factor) } -} -impl core::ops::SubAssign for Volume { - fn sub_assign(&mut self, rhs: Self) { - *self = *self - rhs; + /// Creates a fade effect by interpolating between current volume and target volume. + /// + /// This method performs linear interpolation in the linear domain, which + /// provides a more natural-sounding fade effect. + /// + /// # Arguments + /// * `target` - The target volume to fade towards + /// * `factor` - The interpolation factor (0.0 = current volume, 1.0 = target volume) + /// + /// # Examples + /// ``` + /// use bevy_audio::Volume; + /// + /// let current = Volume::Linear(1.0); + /// let target = Volume::Linear(0.0); + /// let faded = current.fade_towards(target, 0.5); + /// assert_eq!(faded.to_linear(), 0.5); + /// ``` + pub fn fade_towards(&self, target: Volume, factor: f32) -> Self { + let current_linear = self.to_linear(); + let target_linear = target.to_linear(); + let factor_clamped = factor.clamp(0.0, 1.0); + + let interpolated = current_linear + (target_linear - current_linear) * factor_clamped; + Volume::Linear(interpolated) } } @@ -337,8 +375,9 @@ mod tests { Linear(f32::NEG_INFINITY).to_decibels().is_infinite(), "Negative infinite linear scale is equivalent to infinite decibels" ); - assert!( - Decibels(f32::NEG_INFINITY).to_linear().abs() == 0.0, + assert_eq!( + Decibels(f32::NEG_INFINITY).to_linear().abs(), + 0.0, "Negative infinity decibels is equivalent to zero linear scale" ); @@ -361,6 +400,74 @@ mod tests { ); } + #[test] + fn test_increase_by_percentage() { + let volume = Linear(1.0); + + // 100% increase should double the volume + let increased = volume.increase_by_percentage(100.0); + assert_eq!(increased.to_linear(), 2.0); + + // 50% increase + let increased = volume.increase_by_percentage(50.0); + assert_eq!(increased.to_linear(), 1.5); + } + + #[test] + fn test_decrease_by_percentage() { + let volume = Linear(1.0); + + // 50% decrease should halve the volume + let decreased = volume.decrease_by_percentage(50.0); + assert_eq!(decreased.to_linear(), 0.5); + + // 25% decrease + let decreased = volume.decrease_by_percentage(25.0); + assert_eq!(decreased.to_linear(), 0.75); + + // 100% decrease should result in silence + let decreased = volume.decrease_by_percentage(100.0); + assert_eq!(decreased.to_linear(), 0.0); + } + + #[test] + fn test_scale_to_factor() { + let volume = Linear(0.8); + let scaled = volume.scale_to_factor(1.25); + assert_eq!(scaled.to_linear(), 1.0); + } + + #[test] + fn test_fade_towards() { + let current = Linear(1.0); + let target = Linear(0.0); + + // 50% fade should result in 0.5 linear volume + let faded = current.fade_towards(target, 0.5); + assert_eq!(faded.to_linear(), 0.5); + + // 0% fade should keep current volume + let faded = current.fade_towards(target, 0.0); + assert_eq!(faded.to_linear(), 1.0); + + // 100% fade should reach target volume + let faded = current.fade_towards(target, 1.0); + assert_eq!(faded.to_linear(), 0.0); + } + + #[test] + fn test_decibel_math_properties() { + let volume = Linear(1.0); + + // Adding 20dB should multiply linear volume by 10 + let adjusted = volume * Decibels(20.0); + assert_approx_eq(adjusted, Linear(10.0)); + + // Subtracting 20dB should divide linear volume by 10 + let adjusted = volume / Decibels(20.0); + assert_approx_eq(adjusted, Linear(0.1)); + } + fn assert_approx_eq(a: Volume, b: Volume) { const EPSILON: f32 = 0.0001; @@ -380,52 +487,6 @@ mod tests { } } - #[test] - fn volume_ops_add() { - // Linear to Linear. - assert_approx_eq(Linear(0.5) + Linear(0.5), Linear(1.0)); - assert_approx_eq(Linear(0.5) + Linear(0.1), Linear(0.6)); - assert_approx_eq(Linear(0.5) + Linear(-0.5), Linear(0.0)); - - // Decibels to Decibels. - assert_approx_eq(Decibels(0.0) + Decibels(0.0), Decibels(6.0206003)); - assert_approx_eq(Decibels(6.0) + Decibels(6.0), Decibels(12.020599)); - assert_approx_eq(Decibels(-6.0) + Decibels(-6.0), Decibels(0.020599423)); - - // {Linear, Decibels} favors the left hand side of the operation. - assert_approx_eq(Linear(0.5) + Decibels(0.0), Linear(1.5)); - assert_approx_eq(Decibels(0.0) + Linear(0.5), Decibels(3.521825)); - } - - #[test] - fn volume_ops_add_assign() { - // Linear to Linear. - let mut volume = Linear(0.5); - volume += Linear(0.5); - assert_approx_eq(volume, Linear(1.0)); - } - - #[test] - fn volume_ops_sub() { - // Linear to Linear. - assert_approx_eq(Linear(0.5) - Linear(0.5), Linear(0.0)); - assert_approx_eq(Linear(0.5) - Linear(0.1), Linear(0.4)); - assert_approx_eq(Linear(0.5) - Linear(-0.5), Linear(1.0)); - - // Decibels to Decibels. - assert_eq!(Decibels(0.0) - Decibels(0.0), Decibels(f32::NEG_INFINITY)); - assert_approx_eq(Decibels(6.0) - Decibels(4.0), Decibels(-7.736506)); - assert_eq!(Decibels(-6.0) - Decibels(-6.0), Decibels(f32::NEG_INFINITY)); - } - - #[test] - fn volume_ops_sub_assign() { - // Linear to Linear. - let mut volume = Linear(0.5); - volume -= Linear(0.5); - assert_approx_eq(volume, Linear(0.0)); - } - #[test] fn volume_ops_mul() { // Linear to Linear. diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 9b6d7d8cf6..298f80e54f 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -1,19 +1,19 @@ [package] name = "bevy_color" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Types for representing and manipulating color values" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "color"] rust-version = "1.85.0" [dependencies] -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "curve", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } bytemuck = { version = "1", features = ["derive"] } serde = { version = "1.0", features = [ "derive", diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index b29fce72ac..1579519274 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -92,7 +92,7 @@ impl Hsla { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Hsla::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index e5f5ecab32..2a3b115bb3 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -96,7 +96,7 @@ impl Lcha { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Lcha::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 712da5d7ec..d5d72d1544 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -262,6 +262,7 @@ macro_rules! impl_componentwise_vector_space { } impl bevy_math::VectorSpace for $ty { + type Scalar = f32; const ZERO: Self = Self { $($element: 0.0,)+ }; diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 91ffe422c7..ba52a519ae 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -92,7 +92,7 @@ impl Oklcha { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Oklcha::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index ead2adf039..a811e8e313 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -177,8 +177,8 @@ impl Srgba { pub fn to_hex(&self) -> String { let [r, g, b, a] = self.to_u8_array(); match a { - 255 => format!("#{:02X}{:02X}{:02X}", r, g, b), - _ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a), + 255 => format!("#{r:02X}{g:02X}{b:02X}"), + _ => format!("#{r:02X}{g:02X}{b:02X}{a:02X}"), } } diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 304c007104..9b3f158af2 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "bevy_core_pipeline" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Bevy Contributors ", "Carter Anderson ", ] description = "Provides a core render pipeline for Bevy Engine." -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -20,20 +20,20 @@ tonemapping_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs index 7e7e6c1af7..172de3c393 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -1,12 +1,12 @@ use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; +use bevy_asset::{embedded_asset, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_render::{ extract_component::ExtractComponentPlugin, render_asset::RenderAssetPlugin, render_graph::RenderGraphApp, render_resource::{ - Buffer, BufferDescriptor, BufferUsages, PipelineCache, Shader, SpecializedComputePipelines, + Buffer, BufferDescriptor, BufferUsages, PipelineCache, SpecializedComputePipelines, }, renderer::RenderDevice, ExtractSchedule, Render, RenderApp, RenderSystems, @@ -21,9 +21,7 @@ mod settings; use buffers::{extract_buffers, prepare_buffers, AutoExposureBuffers}; pub use compensation_curve::{AutoExposureCompensationCurve, AutoExposureCompensationCurveError}; use node::AutoExposureNode; -use pipeline::{ - AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline, METERING_SHADER_HANDLE, -}; +use pipeline::{AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline}; pub use settings::AutoExposure; use crate::{ @@ -43,12 +41,7 @@ struct AutoExposureResources { impl Plugin for AutoExposurePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - METERING_SHADER_HANDLE, - "auto_exposure.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "auto_exposure.wgsl"); app.add_plugins(RenderAssetPlugin::::default()) .register_type::() diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs index 06fa118827..28ed6b4ee8 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs @@ -1,7 +1,7 @@ use super::compensation_curve::{ AutoExposureCompensationCurve, AutoExposureCompensationCurveUniform, }; -use bevy_asset::{prelude::*, weak_handle}; +use bevy_asset::{load_embedded_asset, prelude::*}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ @@ -44,9 +44,6 @@ pub enum AutoExposurePass { Average, } -pub const METERING_SHADER_HANDLE: Handle = - weak_handle!("05c84384-afa4-41d9-844e-e9cd5e7609af"); - pub const HISTOGRAM_BIN_COUNT: u64 = 64; impl FromWorld for AutoExposurePipeline { @@ -71,7 +68,7 @@ impl FromWorld for AutoExposurePipeline { ), ), ), - histogram_shader: METERING_SHADER_HANDLE.clone(), + histogram_shader: load_embedded_asset!(world, "auto_exposure.wgsl"), } } } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs index cf6fdd4e24..ae359a8a01 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs @@ -5,7 +5,7 @@ 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; +use bevy_render::{extract_component::ExtractComponent, view::Hdr}; use bevy_utils::default; /// Component that enables auto exposure for an HDR-enabled 2d or 3d camera. @@ -25,6 +25,7 @@ use bevy_utils::default; /// **Auto Exposure requires compute shaders and is not compatible with WebGL2.** #[derive(Component, Clone, Reflect, ExtractComponent)] #[reflect(Component, Default, Clone)] +#[require(Hdr)] pub struct AutoExposure { /// The range of exposure values for the histogram. /// diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 53c54c6d2d..111b6e443b 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -1,5 +1,5 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_ecs::prelude::*; use bevy_render::{ render_resource::{ @@ -12,14 +12,12 @@ use bevy_render::{ use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; -pub const BLIT_SHADER_HANDLE: Handle = weak_handle!("59be3075-c34e-43e7-bf24-c8fe21a0192e"); - /// Adds support for specialized "blit pipelines", which can be used to write one texture to another. pub struct BlitPlugin; impl Plugin for BlitPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, BLIT_SHADER_HANDLE, "blit.wgsl", Shader::from_wgsl); + embedded_asset!(app, "blit.wgsl"); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.allow_ambiguous_resource::>(); @@ -40,6 +38,7 @@ impl Plugin for BlitPlugin { pub struct BlitPipeline { pub texture_bind_group: BindGroupLayout, pub sampler: Sampler, + pub shader: Handle, } impl FromWorld for BlitPipeline { @@ -62,6 +61,7 @@ impl FromWorld for BlitPipeline { BlitPipeline { texture_bind_group, sampler, + shader: load_embedded_asset!(render_world, "blit.wgsl"), } } } @@ -82,7 +82,7 @@ impl SpecializedRenderPipeline for BlitPipeline { layout: vec![self.texture_bind_group.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: BLIT_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: vec![], entry_point: "fs_main".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 544b420bfd..88da2db0cc 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -1,5 +1,6 @@ -use super::{Bloom, BLOOM_SHADER_HANDLE, BLOOM_TEXTURE_FORMAT}; +use super::{Bloom, BLOOM_TEXTURE_FORMAT}; use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, resource::Resource, @@ -26,6 +27,8 @@ pub struct BloomDownsamplingPipeline { /// Layout with a texture, a sampler, and uniforms pub bind_group_layout: BindGroupLayout, pub sampler: Sampler, + /// The shader asset handle. + pub shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -78,6 +81,7 @@ impl FromWorld for BloomDownsamplingPipeline { BloomDownsamplingPipeline { bind_group_layout, sampler, + shader: load_embedded_asset!(world, "bloom.wgsl"), } } } @@ -120,7 +124,7 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { layout, vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: BLOOM_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point, targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index cbd87d11bd..65e51c8472 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -9,7 +9,7 @@ use crate::{ core_3d::graph::{Core3d, Node3d}, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::embedded_asset; use bevy_color::{Gray, LinearRgba}; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_math::{ops, UVec2}; @@ -36,15 +36,13 @@ use upsampling_pipeline::{ prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds, }; -const BLOOM_SHADER_HANDLE: Handle = weak_handle!("c9190ddc-573b-4472-8b21-573cab502b73"); - const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat; pub struct BloomPlugin; impl Plugin for BloomPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, BLOOM_SHADER_HANDLE, "bloom.wgsl", Shader::from_wgsl); + embedded_asset!(app, "bloom.wgsl"); app.register_type::(); app.register_type::(); @@ -123,7 +121,7 @@ impl ViewNode for BloomNode { bloom_settings, upsampling_pipeline_ids, downsampling_pipeline_ids, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { if bloom_settings.intensity == 0.0 { diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index f6ee8dbd1e..435ed037b5 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -1,8 +1,12 @@ use super::downsampling_pipeline::BloomUniforms; -use bevy_ecs::{prelude::Component, query::QueryItem, reflect::ReflectComponent}; +use bevy_ecs::{ + prelude::Component, + query::{QueryItem, With}, + reflect::ReflectComponent, +}; use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; +use bevy_render::{extract_component::ExtractComponent, prelude::Camera, view::Hdr}; /// Applies a bloom effect to an HDR-enabled 2d or 3d camera. /// @@ -26,6 +30,7 @@ use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; /// used in Bevy as well as a visualization of the curve's respective scattering profile. #[derive(Component, Reflect, Clone)] #[reflect(Component, Default, Clone)] +#[require(Hdr)] pub struct Bloom { /// Controls the baseline of how much the image is scattered (default: 0.15). /// @@ -219,18 +224,17 @@ pub enum BloomCompositeMode { impl ExtractComponent for Bloom { type QueryData = (&'static Self, &'static Camera); - type QueryFilter = (); + type QueryFilter = With; type Out = (Self, BloomUniforms); - fn extract_component((bloom, camera): QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component((bloom, camera): QueryItem<'_, '_, Self::QueryData>) -> Option { match ( camera.physical_viewport_rect(), camera.physical_viewport_size(), camera.physical_target_size(), camera.is_active, - camera.hdr, ) { - (Some(URect { min: origin, .. }), Some(size), Some(target_size), true, true) + (Some(URect { min: origin, .. }), Some(size), Some(target_size), true) if size.x != 0 && size.y != 0 => { let threshold = bloom.prefilter.threshold; diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs index e4c4ed4a64..f381e664a9 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs @@ -1,8 +1,8 @@ use super::{ - downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_SHADER_HANDLE, - BLOOM_TEXTURE_FORMAT, + downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_TEXTURE_FORMAT, }; use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, resource::Resource, @@ -27,6 +27,8 @@ pub struct UpsamplingPipelineIds { #[derive(Resource)] pub struct BloomUpsamplingPipeline { pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -54,7 +56,10 @@ impl FromWorld for BloomUpsamplingPipeline { ), ); - BloomUpsamplingPipeline { bind_group_layout } + BloomUpsamplingPipeline { + bind_group_layout, + shader: load_embedded_asset!(world, "bloom.wgsl"), + } } } @@ -105,7 +110,7 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { layout: vec![self.bind_group_layout.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: BLOOM_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: vec![], entry_point: "upsample".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs index 60f355c115..e8cd0c65c6 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs @@ -31,7 +31,7 @@ impl ViewNode for MainOpaquePass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( diff --git a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index 494d4d0f89..4054283a57 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -28,7 +28,7 @@ impl ViewNode for MainTransparentPass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(transparent_phases) = diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 9bcb2b4f80..f5314c736d 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -80,6 +80,7 @@ impl From for Camera3dDepthTextureUsage { Self(value.bits()) } } + impl From for TextureUsages { fn from(value: Camera3dDepthTextureUsage) -> Self { Self::from_bits_truncate(value.0) diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 3b1bc96c90..b19268ac1f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -45,7 +45,7 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( 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 77430e0291..0e9465aafa 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -3,7 +3,7 @@ use crate::{ prepass::{DeferredPrepass, ViewPrepassTextures}, }; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset}; use bevy_ecs::prelude::*; use bevy_math::UVec2; use bevy_render::{ @@ -23,18 +23,11 @@ use bevy_render::{ use super::DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT; -pub const COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE: Handle = - weak_handle!("70d91342-1c43-4b20-973f-aa6ce93aa617"); pub struct CopyDeferredLightingIdPlugin; impl Plugin for CopyDeferredLightingIdPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE, - "copy_deferred_lighting_id.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "copy_deferred_lighting_id.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -137,6 +130,8 @@ impl FromWorld for CopyDeferredLightingIdPipeline { ), ); + let shader = load_embedded_asset!(world, "copy_deferred_lighting_id.wgsl"); + let pipeline_id = world .resource_mut::() @@ -145,7 +140,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { layout: vec![layout.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: COPY_DEFERRED_LIGHTING_ID_SHADER_HANDLE, + shader, shader_defs: vec![], entry_point: "fragment".into(), targets: vec![], diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index ffac1eec6d..e786d2a222 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -36,7 +36,7 @@ impl ViewNode for EarlyDeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { run_deferred_prepass( @@ -74,7 +74,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query; @@ -107,6 +107,7 @@ fn run_deferred_prepass<'w>( render_context: &mut RenderContext<'w>, (camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem< 'w, + '_, ::ViewQuery, >, is_late: bool, diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 5eee57b8bb..c27d81180d 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -15,7 +15,7 @@ //! [Depth of field]: https://en.wikipedia.org/wiki/Depth_of_field use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -69,8 +69,6 @@ use crate::{ fullscreen_vertex_shader::fullscreen_shader_vertex_state, }; -const DOF_SHADER_HANDLE: Handle = weak_handle!("c3580ddc-2cbc-4535-a02b-9a2959066b52"); - /// A plugin that adds support for the depth of field effect to Bevy. pub struct DepthOfFieldPlugin; @@ -206,7 +204,7 @@ enum DofPass { impl Plugin for DepthOfFieldPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, DOF_SHADER_HANDLE, "dof.wgsl", Shader::from_wgsl); + embedded_asset!(app, "dof.wgsl"); app.register_type::(); app.register_type::(); @@ -327,6 +325,8 @@ pub struct DepthOfFieldPipeline { /// The bind group layout shared among all invocations of the depth of field /// shader. global_bind_group_layout: BindGroupLayout, + /// The shader asset handle. + shader: Handle, } impl ViewNode for DepthOfFieldNode { @@ -352,7 +352,7 @@ impl ViewNode for DepthOfFieldNode { view_bind_group_layouts, depth_of_field_uniform_index, auxiliary_dof_texture, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -678,11 +678,13 @@ pub fn prepare_depth_of_field_pipelines( &ViewDepthOfFieldBindGroupLayouts, &Msaa, )>, + asset_server: Res, ) { for (entity, view, depth_of_field, view_bind_group_layouts, msaa) in view_targets.iter() { let dof_pipeline = DepthOfFieldPipeline { view_bind_group_layouts: view_bind_group_layouts.clone(), global_bind_group_layout: global_bind_group_layout.layout.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "dof.wgsl"), }; // We'll need these two flags to create the `DepthOfFieldPipelineKey`s. @@ -800,7 +802,7 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { depth_stencil: None, multisample: default(), fragment: Some(FragmentState { - shader: DOF_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 9e04614276..6c6bc7ccc7 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] pub mod auto_exposure; diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index 331dd2408d..ecf6432c9f 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -7,7 +7,7 @@ use crate::{ prepass::{DepthPrepass, MotionVectorPrepass}, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::embedded_asset; use bevy_ecs::{ component::Component, query::{QueryItem, With}, @@ -19,7 +19,7 @@ use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin}, render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, ShaderType, SpecializedRenderPipelines}, + render_resource::{ShaderType, SpecializedRenderPipelines}, Render, RenderApp, RenderSystems, }; @@ -126,19 +126,12 @@ pub struct MotionBlurUniform { _webgl2_padding: bevy_math::Vec2, } -pub const MOTION_BLUR_SHADER_HANDLE: Handle = - weak_handle!("d9ca74af-fa0a-4f11-b0f2-19613b618b93"); - /// Adds support for per-object motion blur to the app. See [`MotionBlur`] for details. pub struct MotionBlurPlugin; impl Plugin for MotionBlurPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - MOTION_BLUR_SHADER_HANDLE, - "motion_blur.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "motion_blur.wgsl"); + app.add_plugins(( ExtractComponentPlugin::::default(), UniformComponentPlugin::::default(), diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index 4eab4ff7a6..dfd4bca103 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -1,3 +1,4 @@ +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ component::Component, entity::Entity, @@ -16,9 +17,9 @@ use bevy_render::{ }, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, MultisampleState, PipelineCache, PrimitiveState, - RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderDefVal, - ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, - TextureFormat, TextureSampleType, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, + ShaderDefVal, ShaderStages, ShaderType, SpecializedRenderPipeline, + SpecializedRenderPipelines, TextureFormat, TextureSampleType, }, renderer::RenderDevice, view::{ExtractedView, Msaa, ViewTarget}, @@ -26,17 +27,18 @@ use bevy_render::{ use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; -use super::{MotionBlurUniform, MOTION_BLUR_SHADER_HANDLE}; +use super::MotionBlurUniform; #[derive(Resource)] pub struct MotionBlurPipeline { pub(crate) sampler: Sampler, pub(crate) layout: BindGroupLayout, pub(crate) layout_msaa: BindGroupLayout, + pub(crate) shader: Handle, } impl MotionBlurPipeline { - pub(crate) fn new(render_device: &RenderDevice) -> Self { + pub(crate) fn new(render_device: &RenderDevice, shader: Handle) -> Self { let mb_layout = &BindGroupLayoutEntries::sequential( ShaderStages::FRAGMENT, ( @@ -82,6 +84,7 @@ impl MotionBlurPipeline { sampler, layout, layout_msaa, + shader, } } } @@ -89,7 +92,9 @@ impl MotionBlurPipeline { impl FromWorld for MotionBlurPipeline { fn from_world(render_world: &mut bevy_ecs::world::World) -> Self { let render_device = render_world.resource::().clone(); - MotionBlurPipeline::new(&render_device) + + let shader = load_embedded_asset!(render_world, "motion_blur.wgsl"); + MotionBlurPipeline::new(&render_device, shader) } } @@ -125,7 +130,7 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { layout, vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: MOTION_BLUR_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 8dc51e4ed5..5f82e10599 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -61,7 +61,7 @@ impl ViewNode for MsaaWritebackNode { &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (target, blit_pipeline_id, msaa): QueryItem<'w, Self::ViewQuery>, + (target, blit_pipeline_id, msaa): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { if *msaa == Msaa::Off { diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 673bbc5a8b..5b5d038fa0 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -1,8 +1,7 @@ //! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details. use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; -use bevy_ecs::{component::*, prelude::*}; +use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*}; use bevy_math::UVec2; use bevy_platform::collections::HashSet; use bevy_platform::time::Instant; @@ -10,10 +9,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::{ExtractComponent, ExtractComponentPlugin}, + load_shader_library, render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{ - BufferUsages, BufferVec, DynamicUniformBuffer, Shader, ShaderType, TextureUsages, - }, + render_resource::{BufferUsages, BufferVec, DynamicUniformBuffer, ShaderType, TextureUsages}, renderer::{RenderDevice, RenderQueue}, view::Msaa, Render, RenderApp, RenderSystems, @@ -33,10 +31,6 @@ use crate::core_3d::{ /// Module that defines the necessary systems to resolve the OIT buffer and render it to the screen. pub mod resolve; -/// Shader handle for the shader that draws the transparent meshes to the OIT layers buffer. -pub const OIT_DRAW_SHADER_HANDLE: Handle = - weak_handle!("0cd3c764-39b8-437b-86b4-4e45635fc03d"); - /// Used to identify which camera will use OIT to render transparent meshes /// and to configure OIT. // TODO consider supporting multiple OIT techniques like WBOIT, Moment Based OIT, @@ -105,12 +99,7 @@ impl Component for OrderIndependentTransparencySettings { pub struct OrderIndependentTransparencyPlugin; impl Plugin for OrderIndependentTransparencyPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - OIT_DRAW_SHADER_HANDLE, - "oit_draw.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "oit_draw.wgsl"); app.add_plugins(( ExtractComponentPlugin::::default(), diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index 0e5102c954..fe62d0c9b1 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -3,7 +3,7 @@ use crate::{ oit::OrderIndependentTransparencySettings, }; use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; use bevy_derive::Deref; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, @@ -16,7 +16,7 @@ use bevy_render::{ BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, BlendComponent, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, DownlevelFlags, FragmentState, MultisampleState, PipelineCache, PrimitiveState, RenderPipelineDescriptor, - Shader, ShaderDefVal, ShaderStages, TextureFormat, + ShaderDefVal, ShaderStages, TextureFormat, }, renderer::{RenderAdapter, RenderDevice}, view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms}, @@ -26,10 +26,6 @@ use tracing::warn; use super::OitBuffers; -/// Shader handle for the shader that sorts the OIT layers, blends the colors based on depth and renders them to the screen. -pub const OIT_RESOLVE_SHADER_HANDLE: Handle = - weak_handle!("562d2917-eb06-444d-9ade-41de76b0f5ae"); - /// Contains the render node used to run the resolve pass. pub mod node; @@ -40,12 +36,7 @@ pub const OIT_REQUIRED_STORAGE_BUFFERS: u32 = 2; pub struct OitResolvePlugin; impl Plugin for OitResolvePlugin { fn build(&self, app: &mut bevy_app::App) { - load_internal_asset!( - app, - OIT_RESOLVE_SHADER_HANDLE, - "oit_resolve.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "oit_resolve.wgsl"); } fn finish(&self, app: &mut bevy_app::App) { @@ -165,6 +156,7 @@ pub fn queue_oit_resolve_pipeline( ), With, >, + asset_server: Res, // Store the key with the id to make the clean up logic easier. // This also means it will always replace the entry if the key changes so nothing to clean up. mut cached_pipeline_id: Local>, @@ -184,7 +176,7 @@ pub fn queue_oit_resolve_pipeline( } } - let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline); + let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline, &asset_server); let pipeline_id = pipeline_cache.queue_render_pipeline(desc); commands.entity(e).insert(OitResolvePipelineId(pipeline_id)); @@ -202,6 +194,7 @@ pub fn queue_oit_resolve_pipeline( fn specialize_oit_resolve_pipeline( key: OitResolvePipelineKey, resolve_pipeline: &OitResolvePipeline, + asset_server: &AssetServer, ) -> RenderPipelineDescriptor { let format = if key.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -217,7 +210,7 @@ fn specialize_oit_resolve_pipeline( ], fragment: Some(FragmentState { entry_point: "fragment".into(), - shader: OIT_RESOLVE_SHADER_HANDLE, + shader: load_embedded_asset!(asset_server, "oit_resolve.wgsl"), shader_defs: vec![ShaderDefVal::UInt( "LAYER_COUNT".into(), key.layer_count as u32, diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index fddac95066..0f188d1d73 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -3,7 +3,7 @@ //! Currently, this consists only of chromatic aberration. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -20,6 +20,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin}, + load_shader_library, render_asset::{RenderAssetUsages, RenderAssets}, render_graph::{ NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, @@ -46,13 +47,6 @@ use crate::{ fullscreen_vertex_shader, }; -/// The handle to the built-in postprocessing shader `post_process.wgsl`. -const POST_PROCESSING_SHADER_HANDLE: Handle = - weak_handle!("5e8e627a-7531-484d-a988-9a38acb34e52"); -/// The handle to the chromatic aberration shader `chromatic_aberration.wgsl`. -const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle = - weak_handle!("e598550e-71c3-4f5a-ba29-aebc3f88c7b5"); - /// The handle to the default chromatic aberration lookup texture. /// /// This is just a 3x1 image consisting of one red pixel, one green pixel, and @@ -136,6 +130,8 @@ pub struct PostProcessingPipeline { source_sampler: Sampler, /// Specifies how to sample the chromatic aberration gradient. chromatic_aberration_lut_sampler: Sampler, + /// The shader asset handle. + shader: Handle, } /// A key that uniquely identifies a built-in postprocessing pipeline. @@ -188,18 +184,9 @@ pub struct PostProcessingNode; impl Plugin for PostProcessingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - POST_PROCESSING_SHADER_HANDLE, - "post_process.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - CHROMATIC_ABERRATION_SHADER_HANDLE, - "chromatic_aberration.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "chromatic_aberration.wgsl"); + + embedded_asset!(app, "post_process.wgsl"); // Load the default chromatic aberration LUT. let mut assets = app.world_mut().resource_mut::>(); @@ -321,6 +308,7 @@ impl FromWorld for PostProcessingPipeline { bind_group_layout, source_sampler, chromatic_aberration_lut_sampler, + shader: load_embedded_asset!(world, "post_process.wgsl"), } } } @@ -334,7 +322,7 @@ impl SpecializedRenderPipeline for PostProcessingPipeline { layout: vec![self.bind_group_layout.clone()], vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: POST_PROCESSING_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: vec![], entry_point: "fragment_main".into(), targets: vec![Some(ColorTargetState { @@ -364,7 +352,7 @@ impl ViewNode for PostProcessingNode { &self, _: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>, + (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -497,7 +485,7 @@ impl ExtractComponent for ChromaticAberration { type Out = ChromaticAberration; fn extract_component( - chromatic_aberration: QueryItem<'_, Self::QueryData>, + chromatic_aberration: QueryItem<'_, '_, Self::QueryData>, ) -> Option { // Skip the postprocessing phase entirely if the intensity is zero. if chromatic_aberration.intensity > 0.0 { diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index deea2a5fa8..880e2b6892 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -74,11 +74,16 @@ pub struct MotionVectorPrepass; #[reflect(Component, Default)] pub struct DeferredPrepass; +/// View matrices from the previous frame. +/// +/// Useful for temporal rendering techniques that need access to last frame's camera data. #[derive(Component, ShaderType, Clone)] pub struct PreviousViewData { pub view_from_world: Mat4, pub clip_from_world: Mat4, pub clip_from_view: Mat4, + pub world_from_clip: Mat4, + pub view_from_clip: Mat4, } #[derive(Resource, Default)] diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 04cc1890b0..500cc0a42b 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -36,7 +36,7 @@ impl ViewNode for EarlyPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { run_prepass(graph, render_context, view_query, world, "early prepass") @@ -73,7 +73,7 @@ impl ViewNode for LatePrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - query: QueryItem<'w, Self::ViewQuery>, + query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // We only need a late prepass if we have occlusion culling and indirect @@ -112,7 +112,7 @@ fn run_prepass<'w>( _, _, has_deferred, - ): QueryItem<'w, ::ViewQuery>, + ): QueryItem<'w, '_, ::ViewQuery>, world: &'w World, label: &'static str, ) -> Result<(), NodeRunError> { diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index ede50d6d8f..51c6934ece 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -1,5 +1,5 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, query::{QueryItem, With}, @@ -28,25 +28,18 @@ use bevy_render::{ Render, RenderApp, RenderSystems, }; use bevy_transform::components::Transform; -use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE}; +use prepass::SkyboxPrepassPipeline; use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms}; -const SKYBOX_SHADER_HANDLE: Handle = weak_handle!("a66cf9cc-cab8-47f8-ac32-db82fdc4f29b"); - pub mod prepass; pub struct SkyboxPlugin; impl Plugin for SkyboxPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - SKYBOX_PREPASS_SHADER_HANDLE, - "skybox_prepass.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "skybox.wgsl"); + embedded_asset!(app, "skybox_prepass.wgsl"); app.register_type::().add_plugins(( ExtractComponentPlugin::::default(), @@ -76,9 +69,10 @@ impl Plugin for SkyboxPlugin { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + let shader = load_embedded_asset!(render_app.world(), "skybox.wgsl"); let render_device = render_app.world().resource::().clone(); render_app - .insert_resource(SkyboxPipeline::new(&render_device)) + .insert_resource(SkyboxPipeline::new(&render_device, shader)) .init_resource::(); } } @@ -119,7 +113,9 @@ impl ExtractComponent for Skybox { type QueryFilter = (); type Out = (Self, SkyboxUniforms); - fn extract_component((skybox, exposure): QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component( + (skybox, exposure): QueryItem<'_, '_, Self::QueryData>, + ) -> Option { let exposure = exposure .map(Exposure::exposure) .unwrap_or_else(|| Exposure::default().exposure()); @@ -129,7 +125,7 @@ impl ExtractComponent for Skybox { SkyboxUniforms { brightness: skybox.brightness * exposure, transform: Transform::from_rotation(skybox.rotation) - .compute_matrix() + .to_matrix() .inverse(), #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] _wasm_padding_8b: 0, @@ -158,10 +154,11 @@ pub struct SkyboxUniforms { #[derive(Resource)] struct SkyboxPipeline { bind_group_layout: BindGroupLayout, + shader: Handle, } impl SkyboxPipeline { - fn new(render_device: &RenderDevice) -> Self { + fn new(render_device: &RenderDevice, shader: Handle) -> Self { Self { bind_group_layout: render_device.create_bind_group_layout( "skybox_bind_group_layout", @@ -176,6 +173,7 @@ impl SkyboxPipeline { ), ), ), + shader, } } } @@ -196,7 +194,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline { layout: vec![self.bind_group_layout.clone()], push_constant_ranges: Vec::new(), vertex: VertexState { - shader: SKYBOX_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: Vec::new(), entry_point: "skybox_vertex".into(), buffers: Vec::new(), @@ -224,7 +222,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline { alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { - shader: SKYBOX_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: Vec::new(), entry_point: "skybox_fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index 658660bbc6..a027f69f93 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -1,6 +1,6 @@ //! Adds motion vector support to skyboxes. See [`SkyboxPrepassPipeline`] for details. -use bevy_asset::{weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ component::Component, entity::Entity, @@ -30,9 +30,6 @@ use crate::{ Skybox, }; -pub const SKYBOX_PREPASS_SHADER_HANDLE: Handle = - weak_handle!("7a292435-bfe6-4ed9-8d30-73bf7aa673b0"); - /// This pipeline writes motion vectors to the prepass for all [`Skybox`]es. /// /// This allows features like motion blur and TAA to work correctly on the skybox. Without this, for @@ -41,6 +38,7 @@ pub const SKYBOX_PREPASS_SHADER_HANDLE: Handle = #[derive(Resource)] pub struct SkyboxPrepassPipeline { bind_group_layout: BindGroupLayout, + shader: Handle, } /// Used to specialize the [`SkyboxPrepassPipeline`]. @@ -75,6 +73,7 @@ impl FromWorld for SkyboxPrepassPipeline { ), ), ), + shader: load_embedded_asset!(world, "skybox_prepass.wgsl"), } } } @@ -102,7 +101,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { - shader: SKYBOX_PREPASS_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: vec![], entry_point: "fragment".into(), targets: prepass_target_descriptors(key.normal_prepass, true, false), diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index f546ef54d3..19ac3ef7e2 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -1,6 +1,6 @@ use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Assets, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Assets, Handle}; use bevy_ecs::prelude::*; use bevy_image::{CompressedImageFormats, Image, ImageSampler, ImageType}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -8,6 +8,7 @@ use bevy_render::{ camera::Camera, extract_component::{ExtractComponent, ExtractComponentPlugin}, extract_resource::{ExtractResource, ExtractResourcePlugin}, + load_shader_library, render_asset::{RenderAssetUsages, RenderAssets}, render_resource::{ binding_types::{sampler, texture_2d, texture_3d, uniform_buffer}, @@ -27,45 +28,22 @@ mod node; use bevy_utils::default; pub use node::TonemappingNode; -const TONEMAPPING_SHADER_HANDLE: Handle = - weak_handle!("e239c010-c25c-42a1-b4e8-08818764d667"); - -const TONEMAPPING_SHARED_SHADER_HANDLE: Handle = - weak_handle!("61dbc544-4b30-4ca9-83bd-4751b5cfb1b1"); - -const TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("d50e3a70-c85e-4725-a81e-72fc83281145"); - /// 3D LUT (look up table) textures used for tonemapping #[derive(Resource, Clone, ExtractResource)] pub struct TonemappingLuts { - blender_filmic: Handle, - agx: Handle, - tony_mc_mapface: Handle, + pub blender_filmic: Handle, + pub agx: Handle, + pub tony_mc_mapface: Handle, } pub struct TonemappingPlugin; impl Plugin for TonemappingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - TONEMAPPING_SHADER_HANDLE, - "tonemapping.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - TONEMAPPING_SHARED_SHADER_HANDLE, - "tonemapping_shared.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - TONEMAPPING_LUT_BINDINGS_SHADER_HANDLE, - "lut_bindings.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "tonemapping_shared.wgsl"); + load_shader_library!(app, "lut_bindings.wgsl"); + + embedded_asset!(app, "tonemapping.wgsl"); if !app.world().is_resource_added::() { let mut images = app.world_mut().resource_mut::>(); @@ -134,6 +112,7 @@ impl Plugin for TonemappingPlugin { pub struct TonemappingPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, + shader: Handle, } /// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity. @@ -296,7 +275,7 @@ impl SpecializedRenderPipeline for TonemappingPipeline { layout: vec![self.texture_bind_group.clone()], vertex: fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: TONEMAPPING_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -340,6 +319,7 @@ impl FromWorld for TonemappingPipeline { TonemappingPipeline { texture_bind_group: tonemap_texture_bind_group, sampler, + shader: load_embedded_asset!(render_world, "tonemapping.wgsl"), } } } diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml new file mode 100644 index 0000000000..e93891d8f7 --- /dev/null +++ b/crates/bevy_core_widgets/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "bevy_core_widgets" +version = "0.17.0-dev" +edition = "2024" +description = "Unstyled common widgets for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ + "bevy_ui_picking_backend", +] } + +# other +accesskit = "0.19" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs new file mode 100644 index 0000000000..97b15b878d --- /dev/null +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -0,0 +1,130 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::query::Has; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; +use bevy_ui::{InteractionDisabled, Pressed}; + +/// Headless button widget. This widget maintains a "pressed" state, which is used to +/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` +/// event when the button is un-pressed. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] +pub struct CoreButton { + /// Optional system to run when the button is clicked, or when the Enter or Space key + /// is pressed while the button is focused. If this field is `None`, the button will + /// emit a `ButtonClicked` event when clicked. + pub on_click: Option, +} + +fn button_on_key_event( + mut trigger: On>, + q_state: Query<(&CoreButton, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, disabled)) = q_state.get(trigger.target()) { + if !disabled { + let event = &trigger.event().input; + if !event.repeat + && event.state == ButtonState::Pressed + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + if let Some(on_click) = bstate.on_click { + trigger.propagate(false); + commands.run_system(on_click); + } + } + } + } +} + +fn button_on_pointer_click( + mut trigger: On>, + mut q_state: Query<(&CoreButton, Has, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if pressed && !disabled { + if let Some(on_click) = bstate.on_click { + commands.run_system(on_click); + } + } + } +} + +fn button_on_pointer_down( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && !pressed { + commands.entity(button).insert(Pressed); + } + } +} + +fn button_on_pointer_up( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_drag_end( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_cancel( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +/// Plugin that adds the observers for the [`CoreButton`] widget. +pub struct CoreButtonPlugin; + +impl Plugin for CoreButtonPlugin { + fn build(&self, app: &mut App) { + app.add_observer(button_on_key_event) + .add_observer(button_on_pointer_down) + .add_observer(button_on_pointer_up) + .add_observer(button_on_pointer_click) + .add_observer(button_on_pointer_drag_end) + .add_observer(button_on_pointer_cancel); + } +} diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs new file mode 100644 index 0000000000..fc12811055 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -0,0 +1,179 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::query::{Has, Without}; +use bevy_ecs::system::{In, ResMut}; +use bevy_ecs::{ + component::Component, + observer::On, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checkable, Checked, InteractionDisabled}; + +/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current +/// state of the checkbox. The `on_change` field is an optional system id that will be run when the +/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is +/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox +/// will update its own [`Checked`] state directly. +/// +/// # Toggle switches +/// +/// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you +/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with +/// the `Switch` role instead of the `Checkbox` role. +#[derive(Component, Debug, Default)] +#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] +pub struct CoreCheckbox { + /// One-shot system that is run when the checkbox state needs to be changed. + pub on_change: Option>>, +} + +fn checkbox_on_key_input( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has), Without>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) { + let event = &ev.event().input; + if event.state == ButtonState::Pressed + && !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + ev.propagate(false); + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +fn checkbox_on_pointer_click( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = Some(ev.target()); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + + ev.propagate(false); + if !disabled { + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +/// Event which can be triggered on a checkbox to set the checked state. This can be used to control +/// the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, SetChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(SetChecked(true), checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct SetChecked(pub bool); + +/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to +/// control the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, ToggleChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(ToggleChecked, checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct ToggleChecked; + +fn checkbox_on_set_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + let will_be_checked = ev.event().0; + if will_be_checked != is_checked { + set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked); + } + } +} + +fn checkbox_on_toggle_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } +} + +fn set_checkbox_state( + commands: &mut Commands, + entity: impl Into, + checkbox: &CoreCheckbox, + new_state: bool, +) { + if let Some(on_change) = checkbox.on_change { + commands.run_system_with(on_change, new_state); + } else if new_state { + commands.entity(entity.into()).insert(Checked); + } else { + commands.entity(entity.into()).remove::(); + } +} + +/// Plugin that adds the observers for the [`CoreCheckbox`] widget. +pub struct CoreCheckboxPlugin; + +impl Plugin for CoreCheckboxPlugin { + fn build(&self, app: &mut App) { + app.add_observer(checkbox_on_key_input) + .add_observer(checkbox_on_pointer_click) + .add_observer(checkbox_on_set_checked) + .add_observer(checkbox_on_toggle_checked); + } +} diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs new file mode 100644 index 0000000000..5a6b90636a --- /dev/null +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -0,0 +1,488 @@ +use core::ops::RangeInclusive; + +use accesskit::{Orientation, Role}; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::hierarchy::Children; +use bevy_ecs::lifecycle::Insert; +use bevy_ecs::query::Has; +use bevy_ecs::system::{In, Res}; +use bevy_ecs::world::DeferredWorld; +use bevy_ecs::{ + component::Component, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_log::warn_once; +use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; +use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; + +/// Defines how the slider should behave when you click on the track (not the thumb). +#[derive(Debug, Default)] +pub enum TrackClick { + /// Clicking on the track lets you drag to edit the value, just like clicking on the thumb. + #[default] + Drag, + /// Clicking on the track increments or decrements the slider by [`SliderStep`]. + Step, + /// Clicking on the track snaps the value to the clicked position. + Snap, +} + +/// A headless slider widget, which can be used to build custom sliders. Sliders have a value +/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An +/// optional step size can be specified via [`SliderStep`]. +/// +/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This +/// can be useful in a console environment for controlling the value gamepad inputs. +/// +/// The presence of the `on_change` property controls whether the slider uses internal or external +/// state management. If the `on_change` property is `None`, then the slider updates its own state +/// automatically. Otherwise, the `on_change` property contains the id of a one-shot system which is +/// passed the new slider value. In this case, the slider value is not modified, it is the +/// responsibility of the callback to trigger whatever data-binding mechanism is used to update the +/// slider's value. +/// +/// Typically a slider will contain entities representing the "track" and "thumb" elements. The core +/// slider makes no assumptions about the hierarchical structure of these elements, but expects that +/// the thumb will be marked with a [`CoreSliderThumb`] component. +/// +/// The core slider does not modify the visible position of the thumb: that is the responsibility of +/// the stylist. This can be done either in percent or pixel units as desired. To prevent overhang +/// at the ends of the slider, the positioning should take into account the thumb width, by reducing +/// the amount of travel. So for example, in a slider 100px wide, with a thumb that is 10px, the +/// amount of travel is 90px. The core slider's calculations for clicking and dragging assume this +/// is the case, and will reduce the travel by the measured size of the thumb entity, which allows +/// the movement of the thumb to be perfectly synchronized with the movement of the mouse. +/// +/// In cases where overhang is desired for artistic reasons, the thumb may have additional +/// decorative child elements, absolutely positioned, which don't affect the size measurement. +#[derive(Component, Debug, Default)] +#[require( + AccessibilityNode(accesskit::Node::new(Role::Slider)), + CoreSliderDragState, + SliderValue, + SliderRange, + SliderStep +)] +pub struct CoreSlider { + /// Callback which is called when the slider is dragged or the value is changed via other user + /// interaction. If this value is `None`, then the slider will self-update. + pub on_change: Option>>, + /// Set the track-clicking behavior for this slider. + pub track_click: TrackClick, + // TODO: Think about whether we want a "vertical" option. +} + +/// Marker component that identifies which descendant element is the slider thumb. +#[derive(Component, Debug, Default)] +pub struct CoreSliderThumb; + +/// A component which stores the current value of the slider. +#[derive(Component, Debug, Default, PartialEq, Clone, Copy)] +#[component(immutable)] +pub struct SliderValue(pub f32); + +/// A component which represents the allowed range of the slider value. Defaults to 0.0..=1.0. +#[derive(Component, Debug, PartialEq, Clone, Copy)] +#[component(immutable)] +pub struct SliderRange { + start: f32, + end: f32, +} + +impl SliderRange { + /// Creates a new slider range with the given start and end values. + pub fn new(start: f32, end: f32) -> Self { + if end < start { + warn_once!( + "Expected SliderRange::start ({}) <= SliderRange::end ({})", + start, + end + ); + } + Self { start, end } + } + + /// Creates a new slider range from a Rust range. + pub fn from_range(range: RangeInclusive) -> Self { + let (start, end) = range.into_inner(); + Self { start, end } + } + + /// Returns the minimum allowed value for this slider. + pub fn start(&self) -> f32 { + self.start + } + + /// Return a new instance of a `SliderRange` with a new start position. + pub fn with_start(&self, start: f32) -> Self { + Self::new(start, self.end) + } + + /// Returns the maximum allowed value for this slider. + pub fn end(&self) -> f32 { + self.end + } + + /// Return a new instance of a `SliderRange` with a new end position. + pub fn with_end(&self, end: f32) -> Self { + Self::new(self.start, end) + } + + /// Returns the full span of the range (max - min). + pub fn span(&self) -> f32 { + self.end - self.start + } + + /// Returns the center value of the range. + pub fn center(&self) -> f32 { + (self.start + self.end) / 2.0 + } + + /// Constrain a value between the minimum and maximum allowed values for this slider. + pub fn clamp(&self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Compute the position of the thumb on the slider, as a value between 0 and 1, taking + /// into account the proportion of the value between the minimum and maximum limits. + pub fn thumb_position(&self, value: f32) -> f32 { + if self.end > self.start { + (value - self.start) / (self.end - self.start) + } else { + 0.5 + } + } +} + +impl Default for SliderRange { + fn default() -> Self { + Self { + start: 0.0, + end: 1.0, + } + } +} + +/// Defines the amount by which to increment or decrement the slider value when using keyboard +/// shorctuts. Defaults to 1.0. +#[derive(Component, Debug, PartialEq, Clone)] +#[component(immutable)] +pub struct SliderStep(pub f32); + +impl Default for SliderStep { + fn default() -> Self { + Self(1.0) + } +} + +/// Component used to manage the state of a slider during dragging. +#[derive(Component, Default)] +pub struct CoreSliderDragState { + /// Whether the slider is currently being dragged. + pub dragging: bool, + + /// The value of the slider when dragging started. + offset: f32, +} + +pub(crate) fn slider_on_pointer_down( + mut trigger: On>, + q_slider: Query<( + &CoreSlider, + &SliderValue, + &SliderRange, + &SliderStep, + &ComputedNode, + &ComputedNodeTarget, + &UiGlobalTransform, + Has, + )>, + q_thumb: Query<&ComputedNode, With>, + q_children: Query<&Children>, + mut commands: Commands, + ui_scale: Res, +) { + if q_thumb.contains(trigger.target()) { + // Thumb click, stop propagation to prevent track click. + trigger.propagate(false); + } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = + q_slider.get(trigger.target()) + { + // Track click + trigger.propagate(false); + + if disabled { + return; + } + + // Find thumb size by searching descendants for the first entity with CoreSliderThumb + let thumb_size = q_children + .iter_descendants(trigger.target()) + .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .unwrap_or(0.0); + + // Detect track click. + let local_pos = transform.try_inverse().unwrap().transform_point2( + trigger.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, + ); + let track_width = node.size().x - thumb_size; + // Avoid division by zero + let click_val = if track_width > 0. { + local_pos.x * range.span() / track_width + range.center() + } else { + 0. + }; + + // Compute new value from click position + let new_value = range.clamp(match slider.track_click { + TrackClick::Drag => { + return; + } + TrackClick::Step => { + if click_val < value.0 { + value.0 - step.0 + } else { + value.0 + step.0 + } + } + TrackClick::Snap => click_val, + }); + + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } +} + +pub(crate) fn slider_on_drag_start( + mut trigger: On>, + mut q_slider: Query< + ( + &SliderValue, + &mut CoreSliderDragState, + Has, + ), + With, + >, +) { + if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled { + drag.dragging = true; + drag.offset = value.0; + } + } +} + +pub(crate) fn slider_on_drag( + mut trigger: On>, + mut q_slider: Query<( + &ComputedNode, + &CoreSlider, + &SliderRange, + &UiGlobalTransform, + &mut CoreSliderDragState, + Has, + )>, + q_thumb: Query<&ComputedNode, With>, + q_children: Query<&Children>, + mut commands: Commands, + ui_scale: Res, +) { + if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target()) + { + trigger.propagate(false); + if drag.dragging && !disabled { + let mut distance = trigger.event().distance / ui_scale.0; + distance.y *= -1.; + let distance = transform.transform_vector2(distance); + // Find thumb size by searching descendants for the first entity with CoreSliderThumb + let thumb_size = q_children + .iter_descendants(trigger.target()) + .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .unwrap_or(0.0); + let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); + let span = range.span(); + let new_value = if span > 0. { + range.clamp(drag.offset + (distance.x * span) / slider_width) + } else { + range.start() + span * 0.5 + }; + + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } + } +} + +pub(crate) fn slider_on_drag_end( + mut trigger: On>, + mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>, +) { + if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target()) { + trigger.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn slider_on_key_input( + mut trigger: On>, + q_slider: Query<( + &CoreSlider, + &SliderValue, + &SliderRange, + &SliderStep, + Has, + )>, + mut commands: Commands, +) { + if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target()) { + let event = &trigger.event().input; + if !disabled && event.state == ButtonState::Pressed { + let new_value = match event.key_code { + KeyCode::ArrowLeft => range.clamp(value.0 - step.0), + KeyCode::ArrowRight => range.clamp(value.0 + step.0), + KeyCode::Home => range.start(), + KeyCode::End => range.end(), + _ => { + return; + } + }; + trigger.propagate(false); + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } + } +} + +pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_orientation(Orientation::Horizontal); + } +} + +pub(crate) fn slider_on_insert_value(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let value = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value(value.into()); + } +} + +pub(crate) fn slider_on_insert_range(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let range = *entity.get::().unwrap(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_min_numeric_value(range.start().into()); + accessibility.set_max_numeric_value(range.end().into()); + } +} + +pub(crate) fn slider_on_insert_step(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let step = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value_step(step.into()); + } +} + +/// An [`EntityEvent`] that can be triggered on a slider to modify its value (using the `on_change` callback). +/// This can be used to control the slider via gamepad buttons or other inputs. The value will be +/// clamped when the event is processed. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, SetSliderValue}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a slider +/// let slider = commands.spawn(( +/// CoreSlider::default(), +/// SliderValue(0.5), +/// SliderRange::new(0.0, 1.0), +/// )).id(); +/// +/// // Set to an absolute value +/// commands.trigger_targets(SetSliderValue::Absolute(0.75), slider); +/// +/// // Adjust relatively +/// commands.trigger_targets(SetSliderValue::Relative(-0.25), slider); +/// } +/// ``` +#[derive(Event, EntityEvent, Clone)] +pub enum SetSliderValue { + /// Set the slider value to a specific value. + Absolute(f32), + /// Add a delta to the slider value. + Relative(f32), + /// Add a delta to the slider value, multiplied by the step size. + RelativeStep(f32), +} + +fn slider_on_set_value( + mut trigger: On, + q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>, + mut commands: Commands, +) { + if let Ok((slider, value, range, step)) = q_slider.get(trigger.target()) { + trigger.propagate(false); + let new_value = match trigger.event() { + SetSliderValue::Absolute(new_value) => range.clamp(*new_value), + SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta), + SetSliderValue::RelativeStep(delta) => { + range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default()) + } + }; + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } +} + +/// Plugin that adds the observers for the [`CoreSlider`] widget. +pub struct CoreSliderPlugin; + +impl Plugin for CoreSliderPlugin { + fn build(&self, app: &mut App) { + app.add_observer(slider_on_pointer_down) + .add_observer(slider_on_drag_start) + .add_observer(slider_on_drag_end) + .add_observer(slider_on_drag) + .add_observer(slider_on_key_input) + .add_observer(slider_on_insert) + .add_observer(slider_on_insert_value) + .add_observer(slider_on_insert_range) + .add_observer(slider_on_insert_step) + .add_observer(slider_on_set_value); + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs new file mode 100644 index 0000000000..cdb9142b52 --- /dev/null +++ b/crates/bevy_core_widgets/src/lib.rs @@ -0,0 +1,38 @@ +//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders. +//! These widgets have no inherent styling, it's the responsibility of the user to add styling +//! appropriate for their game or application. +//! +//! # State Management +//! +//! Most of the widgets use external state management: this means that the widgets do not +//! automatically update their own internal state, but instead rely on the app to update the widget +//! state (as well as any other related game state) in response to a change event emitted by the +//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the +//! user interface is showing a live view of dynamic data coming from deeper within the game engine. + +// Note on naming: the `Core` prefix is used on components that would normally be internal to the +// styled/opinionated widgets that use them. Components which are directly exposed to users above +// the widget level, like `SliderValue`, should not have the `Core` prefix. + +mod core_button; +mod core_checkbox; +mod core_slider; + +use bevy_app::{App, Plugin}; + +pub use core_button::{CoreButton, CoreButtonPlugin}; +pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; +pub use core_slider::{ + CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue, + SliderRange, SliderStep, SliderValue, TrackClick, +}; + +/// A plugin that registers the observers for all of the core widgets. If you don't want to +/// use all of the widgets, you can import the individual widget plugins instead. +pub struct CoreWidgetsPlugin; + +impl Plugin for CoreWidgetsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin)); + } +} diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index 1c4cb4adcc..f1a1cd44b3 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides derive implementations for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.17.0-dev" } quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_derive/compile_fail/Cargo.toml b/crates/bevy_derive/compile_fail/Cargo.toml index a9ad3e95e1..e9116dc57b 100644 --- a/crates/bevy_derive/compile_fail/Cargo.toml +++ b/crates/bevy_derive/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_derive_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's various macros" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" publish = false diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs index 2636ffc57d..16a66eb906 100644 --- a/crates/bevy_derive/src/lib.rs +++ b/crates/bevy_derive/src/lib.rs @@ -1,9 +1,10 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +//! Assorted proc macro derive functions. + #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] extern crate proc_macro; @@ -188,11 +189,34 @@ pub fn derive_deref_mut(input: TokenStream) -> TokenStream { derefs::derive_deref_mut(input) } +/// Generates the required main function boilerplate for Android. #[proc_macro_attribute] pub fn bevy_main(attr: TokenStream, item: TokenStream) -> TokenStream { bevy_main::bevy_main(attr, item) } +/// Adds `enum_variant_index` and `enum_variant_name` functions to enums. +/// +/// # Example +/// +/// ``` +/// use bevy_derive::{EnumVariantMeta}; +/// +/// #[derive(EnumVariantMeta)] +/// enum MyEnum { +/// A, +/// B, +/// } +/// +/// let a = MyEnum::A; +/// let b = MyEnum::B; +/// +/// assert_eq!(0, a.enum_variant_index()); +/// assert_eq!("A", a.enum_variant_name()); +/// +/// assert_eq!(1, b.enum_variant_index()); +/// assert_eq!("B", b.enum_variant_name()); +/// ``` #[proc_macro_derive(EnumVariantMeta)] pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream { enum_variant_meta::derive_enum_variant_meta(input) @@ -205,8 +229,6 @@ pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream { pub fn derive_app_label(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::DeriveInput); let mut trait_path = BevyManifest::shared().get_path("bevy_app"); - let mut dyn_eq_path = trait_path.clone(); trait_path.segments.push(format_ident!("AppLabel").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "AppLabel", &trait_path, &dyn_eq_path) + derive_label(input, "AppLabel", &trait_path) } diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index ad0f2c515c..ef31767c0e 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_dev_tools" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Collection of developer tools for the Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -13,25 +13,25 @@ bevy_ci_testing = ["serde", "ron"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_state = { path = "../bevy_state", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_state = { path = "../bevy_state", version = "0.17.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } -ron = { version = "0.8.0", optional = true } +ron = { version = "0.10", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] diff --git a/crates/bevy_dev_tools/src/ci_testing/config.rs b/crates/bevy_dev_tools/src/ci_testing/config.rs index 6dc601f1cc..a2419dfaa5 100644 --- a/crates/bevy_dev_tools/src/ci_testing/config.rs +++ b/crates/bevy_dev_tools/src/ci_testing/config.rs @@ -49,7 +49,7 @@ pub enum CiTestingEvent { } /// A custom event that can be configured from a configuration file for CI testing. -#[derive(Event)] +#[derive(Event, BufferedEvent)] pub struct CiTestingCustomEvent(pub String); #[cfg(test)] diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index 1dfd473409..5e826e3f9c 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -1,11 +1,11 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] -//! This crate provides additional utilities for the [Bevy game engine](https://bevyengine.org), +//! This crate provides additional utilities for the [Bevy game engine](https://bevy.org), //! focused on improving developer experience. use bevy_app::prelude::*; diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index 8e4ee7ae86..16233cd3dc 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -1,14 +1,13 @@ //! Text and on-screen debugging tools use bevy_app::prelude::*; -use bevy_asset::prelude::*; use bevy_color::prelude::*; use bevy_ecs::prelude::*; use bevy_picking::backend::HitData; use bevy_picking::hover::HoverMap; -use bevy_picking::pointer::{Location, PointerId, PointerPress}; +use bevy_picking::pointer::{Location, PointerId, PointerInput, PointerLocation, PointerPress}; use bevy_picking::prelude::*; -use bevy_picking::{pointer, PickingSystems}; +use bevy_picking::PickingSystems; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_text::prelude::*; @@ -92,11 +91,11 @@ impl Plugin for DebugPickingPlugin { ( // This leaves room to easily change the log-level associated // with different events, should that be desired. - log_event_debug::.run_if(DebugPickingMode::is_noisy), + log_event_debug::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, log_pointer_event_debug::, - log_pointer_event_debug::, - log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, log_pointer_event_debug::, log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, @@ -122,7 +121,7 @@ impl Plugin for DebugPickingPlugin { } /// Listen for any event and logs it at the debug level -pub fn log_event_debug(mut events: EventReader) { +pub fn log_event_debug(mut events: EventReader) { for event in events.read() { debug!("{event:?}"); } @@ -215,7 +214,7 @@ pub fn update_debug_data( entity_names: Query, mut pointers: Query<( &PointerId, - &pointer::PointerLocation, + &PointerLocation, &PointerPress, &mut PointerDebug, )>, @@ -248,25 +247,18 @@ pub fn debug_draw( pointers: Query<(Entity, &PointerId, &PointerDebug)>, scale: Res, ) { - let font_handle: Handle = Default::default(); - for (entity, id, debug) in pointers.iter() { + for (entity, id, debug) in &pointers { let Some(pointer_location) = &debug.location else { continue; }; let text = format!("{id:?}\n{debug}"); - for camera in camera_query - .iter() - .map(|(entity, camera)| { - ( - entity, - camera.target.normalize(primary_window.single().ok()), - ) - }) - .filter_map(|(entity, target)| Some(entity).zip(target)) - .filter(|(_entity, target)| target == &pointer_location.target) - .map(|(cam_entity, _target)| cam_entity) - { + for (camera, _) in camera_query.iter().filter(|(_, camera)| { + camera + .target + .normalize(primary_window.single().ok()) + .is_some_and(|target| target == pointer_location.target) + }) { let mut pointer_pos = pointer_location.position; if let Some(viewport) = camera_query .get(camera) @@ -278,23 +270,21 @@ pub fn debug_draw( commands .entity(entity) + .despawn_related::() .insert(( - Text::new(text.clone()), - TextFont { - font: font_handle.clone(), - font_size: 12.0, - ..Default::default() - }, - TextColor(Color::WHITE), Node { position_type: PositionType::Absolute, left: Val::Px(pointer_pos.x + 5.0) / scale.0, top: Val::Px(pointer_pos.y + 5.0) / scale.0, + padding: UiRect::px(10.0, 10.0, 8.0, 6.0), ..Default::default() }, - )) - .insert(Pickable::IGNORE) - .insert(UiTargetCamera(camera)); + BackgroundColor(Color::BLACK.with_alpha(0.75)), + GlobalZIndex(i32::MAX), + Pickable::IGNORE, + UiTargetCamera(camera), + children![(Text::new(text.clone()), TextFont::from_font_size(12.0))], + )); } } } diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index eff1710438..e930da149a 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_diagnostic" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides diagnostic functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -18,7 +18,6 @@ serialize = [ "dep:serde", "bevy_ecs/serialize", "bevy_time/serialize", - "bevy_utils/serde", "bevy_platform/serialize", ] @@ -39,7 +38,6 @@ std = [ "bevy_app/std", "bevy_platform/std", "bevy_time/std", - "bevy_utils/std", "bevy_tasks/std", ] @@ -50,20 +48,17 @@ critical-section = [ "bevy_app/critical-section", "bevy_platform/critical-section", "bevy_time/critical-section", - "bevy_utils/critical-section", "bevy_tasks/critical-section", ] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ - "alloc", -] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index 00a758416b..1f67d5220a 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -113,7 +113,9 @@ impl core::fmt::Display for DiagnosticPath { /// A single measurement of a [`Diagnostic`]. #[derive(Debug)] pub struct DiagnosticMeasurement { + /// When this measurement was taken. pub time: Instant, + /// Value of the measurement. pub value: f64, } @@ -122,12 +124,14 @@ pub struct DiagnosticMeasurement { #[derive(Debug)] pub struct Diagnostic { path: DiagnosticPath, + /// Suffix to use when logging measurements for this [`Diagnostic`], for example to show units. pub suffix: Cow<'static, str>, history: VecDeque, sum: f64, ema: f64, ema_smoothing_factor: f64, max_history_length: usize, + /// Disabled [`Diagnostic`]s are not measured or logged. pub is_enabled: bool, } @@ -219,6 +223,7 @@ impl Diagnostic { self } + /// Get the [`DiagnosticPath`] that identifies this [`Diagnostic`]. pub fn path(&self) -> &DiagnosticPath { &self.path } @@ -282,10 +287,12 @@ impl Diagnostic { self.max_history_length } + /// All measured values from this [`Diagnostic`], up to the configured maximum history length. pub fn values(&self) -> impl Iterator { self.history.iter().map(|x| &x.value) } + /// All measurements from this [`Diagnostic`], up to the configured maximum history length. pub fn measurements(&self) -> impl Iterator { self.history.iter() } @@ -293,6 +300,8 @@ impl Diagnostic { /// Clear the history of this diagnostic. pub fn clear_history(&mut self) { self.history.clear(); + self.sum = 0.0; + self.ema = 0.0; } } @@ -310,10 +319,12 @@ impl DiagnosticsStore { self.diagnostics.insert(diagnostic.path.clone(), diagnostic); } + /// Get the [`DiagnosticMeasurement`] with the given [`DiagnosticPath`], if it exists. pub fn get(&self, path: &DiagnosticPath) -> Option<&Diagnostic> { self.diagnostics.get(path) } + /// Mutably get the [`DiagnosticMeasurement`] with the given [`DiagnosticPath`], if it exists. pub fn get_mut(&mut self, path: &DiagnosticPath) -> Option<&mut Diagnostic> { self.diagnostics.get_mut(path) } @@ -420,3 +431,31 @@ impl RegisterDiagnostic for App { self } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clear_history() { + const MEASUREMENT: f64 = 20.0; + + let mut diagnostic = + Diagnostic::new(DiagnosticPath::new("test")).with_max_history_length(5); + let mut now = Instant::now(); + + for _ in 0..3 { + for _ in 0..5 { + diagnostic.add_measurement(DiagnosticMeasurement { + time: now, + value: MEASUREMENT, + }); + // Increase time to test smoothed average. + now += Duration::from_secs(1); + } + assert!((diagnostic.average().unwrap() - MEASUREMENT).abs() < 0.1); + assert!((diagnostic.smoothed().unwrap() - MEASUREMENT).abs() < 0.1); + diagnostic.clear_history(); + } + } +} diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs index 91874a390c..b20a82bf6c 100644 --- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs @@ -19,8 +19,10 @@ impl Plugin for EntityCountDiagnosticsPlugin { } impl EntityCountDiagnosticsPlugin { + /// Number of currently allocated entities. pub const ENTITY_COUNT: DiagnosticPath = DiagnosticPath::const_new("entity_count"); + /// Updates entity count measurement. pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) { diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.len() as f64); } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index 22b6176fa2..df195a6122 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -17,11 +17,13 @@ pub struct FrameTimeDiagnosticsPlugin { /// The smoothing factor for the exponential moving average. Usually `2.0 / (history_length + 1.0)`. pub smoothing_factor: f64, } + impl Default for FrameTimeDiagnosticsPlugin { fn default() -> Self { Self::new(DEFAULT_MAX_HISTORY_LENGTH) } } + impl FrameTimeDiagnosticsPlugin { /// Creates a new `FrameTimeDiagnosticsPlugin` with the specified `max_history_length` and a /// reasonable `smoothing_factor`. @@ -58,10 +60,16 @@ impl Plugin for FrameTimeDiagnosticsPlugin { } impl FrameTimeDiagnosticsPlugin { + /// Frames per second. pub const FPS: DiagnosticPath = DiagnosticPath::const_new("fps"); + + /// Total frames since application start. pub const FRAME_COUNT: DiagnosticPath = DiagnosticPath::const_new("frame_count"); + + /// Frame time in ms. pub const FRAME_TIME: DiagnosticPath = DiagnosticPath::const_new("frame_time"); + /// Updates frame count, frame time and fps measurements. pub fn diagnostic_system( mut diagnostics: Diagnostics, time: Res>, diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index e5098d6c6f..707c7c7cc7 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -1,13 +1,12 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] -//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevyengine.org/). +//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevy.org/). //! It allows users to easily add diagnostic functionality to their Bevy applications, enhancing //! their ability to monitor and optimize their game's. @@ -29,7 +28,7 @@ pub use diagnostic::*; pub use entity_count_diagnostics_plugin::EntityCountDiagnosticsPlugin; pub use frame_count_diagnostics_plugin::{update_frame_count, FrameCount, FrameCountPlugin}; pub use frame_time_diagnostics_plugin::FrameTimeDiagnosticsPlugin; -pub use log_diagnostics_plugin::LogDiagnosticsPlugin; +pub use log_diagnostics_plugin::{LogDiagnosticsPlugin, LogDiagnosticsState}; #[cfg(feature = "sysinfo_plugin")] pub use system_information_diagnostics_plugin::{SystemInfo, SystemInformationDiagnosticsPlugin}; diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index 1246b03f81..4175eb395e 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -1,7 +1,8 @@ use super::{Diagnostic, DiagnosticPath, DiagnosticsStore}; -use alloc::vec::Vec; + use bevy_app::prelude::*; use bevy_ecs::prelude::*; +use bevy_platform::collections::HashSet; use bevy_time::{Real, Time, Timer, TimerMode}; use core::time::Duration; use log::{debug, info}; @@ -14,16 +15,76 @@ use log::{debug, info}; /// /// When no diagnostics are provided, this plugin does nothing. pub struct LogDiagnosticsPlugin { + /// If `true` then the `Debug` representation of each `Diagnostic` is logged. + /// If `false` then a (smoothed) current value and historical average are logged. + /// + /// Defaults to `false`. pub debug: bool, + /// Time to wait between logging diagnostics and logging them again. pub wait_duration: Duration, - pub filter: Option>, + /// If `Some` then only these diagnostics are logged. + pub filter: Option>, } /// State used by the [`LogDiagnosticsPlugin`] #[derive(Resource)] -struct LogDiagnosticsState { +pub struct LogDiagnosticsState { timer: Timer, - filter: Option>, + filter: Option>, +} + +impl LogDiagnosticsState { + /// Sets a new duration for the log timer + pub fn set_timer_duration(&mut self, duration: Duration) { + self.timer.set_duration(duration); + self.timer.set_elapsed(Duration::ZERO); + } + + /// Add a filter to the log state, returning `true` if the [`DiagnosticPath`] + /// was not present + pub fn add_filter(&mut self, diagnostic_path: DiagnosticPath) -> bool { + if let Some(filter) = &mut self.filter { + filter.insert(diagnostic_path) + } else { + self.filter = Some(HashSet::from_iter([diagnostic_path])); + true + } + } + + /// Extends the filter of the log state with multiple [`DiagnosticPaths`](DiagnosticPath) + pub fn extend_filter(&mut self, iter: impl IntoIterator) { + if let Some(filter) = &mut self.filter { + filter.extend(iter); + } else { + self.filter = Some(HashSet::from_iter(iter)); + } + } + + /// Removes a filter from the log state, returning `true` if it was present + pub fn remove_filter(&mut self, diagnostic_path: &DiagnosticPath) -> bool { + if let Some(filter) = &mut self.filter { + filter.remove(diagnostic_path) + } else { + false + } + } + + /// Clears the filters of the log state + pub fn clear_filter(&mut self) { + if let Some(filter) = &mut self.filter { + filter.clear(); + } + } + + /// Enables filtering with empty filters + pub fn enable_filtering(&mut self) { + self.filter = Some(HashSet::new()); + } + + /// Disables filtering + pub fn disable_filtering(&mut self) { + self.filter = None; + } } impl Default for LogDiagnosticsPlugin { @@ -52,7 +113,8 @@ impl Plugin for LogDiagnosticsPlugin { } impl LogDiagnosticsPlugin { - pub fn filtered(filter: Vec) -> Self { + /// Filter logging to only the paths in `filter`. + pub fn filtered(filter: HashSet) -> Self { LogDiagnosticsPlugin { filter: Some(filter), ..Default::default() @@ -65,7 +127,7 @@ impl LogDiagnosticsPlugin { mut callback: impl FnMut(&Diagnostic), ) { if let Some(filter) = &state.filter { - for path in filter { + for path in filter.iter() { if let Some(diagnostic) = diagnostics.get(path) { if diagnostic.is_enabled { callback(diagnostic); @@ -128,7 +190,7 @@ impl LogDiagnosticsPlugin { time: Res>, diagnostics: Res, ) { - if state.timer.tick(time.delta()).finished() { + if state.timer.tick(time.delta()).is_finished() { Self::log_diagnostics(&state, &diagnostics); } } @@ -138,7 +200,7 @@ impl LogDiagnosticsPlugin { time: Res>, diagnostics: Res, ) { - if state.timer.tick(time.delta()).finished() { + if state.timer.tick(time.delta()).is_finished() { Self::for_each_diagnostic(&state, &diagnostics, |diagnostic| { debug!("{:#?}\n", diagnostic); }); diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 376a109ae3..768bbb0828 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -46,10 +46,15 @@ impl SystemInformationDiagnosticsPlugin { /// [`SystemInformationDiagnosticsPlugin`] for more information. #[derive(Debug, Resource)] pub struct SystemInfo { + /// OS name and version. pub os: String, + /// System kernel version. pub kernel: String, + /// CPU model name. pub cpu: String, + /// Physical core count. pub core_count: String, + /// System RAM. pub memory: String, } diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index 26aec33b83..334ea10f1f 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_dylib" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Force the Bevy Engine to be dynamically linked for faster linking" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -12,7 +12,7 @@ keywords = ["bevy"] crate-type = ["dylib"] [dependencies] -bevy_internal = { path = "../bevy_internal", version = "0.16.0-dev", default-features = false } +bevy_internal = { path = "../bevy_internal", version = "0.17.0-dev", default-features = false } [lints] workspace = true diff --git a/crates/bevy_dylib/src/lib.rs b/crates/bevy_dylib/src/lib.rs index 1ff40ce3e8..84322813db 100644 --- a/crates/bevy_dylib/src/lib.rs +++ b/crates/bevy_dylib/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Forces dynamic linking of Bevy. diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 97cdcee082..5aa3dfe5ef 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "bevy_ecs" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine's entity component system" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["ecs", "game", "bevy"] categories = ["game-engines", "data-structures"] -rust-version = "1.85.0" +rust-version = "1.86.0" [features] default = ["std", "bevy_reflect", "async_executor", "backtrace"] @@ -20,12 +20,7 @@ default = ["std", "bevy_reflect", "async_executor", "backtrace"] multi_threaded = ["bevy_tasks/multi_threaded", "dep:arrayvec"] ## Adds serialization support through `serde`. -serialize = [ - "dep:serde", - "bevy_utils/serde", - "bevy_platform/serialize", - "indexmap/serde", -] +serialize = ["dep:serde", "bevy_platform/serialize", "indexmap/serde"] ## Adds runtime reflection support using `bevy_reflect`. bevy_reflect = ["dep:bevy_reflect"] @@ -33,13 +28,6 @@ bevy_reflect = ["dep:bevy_reflect"] ## Extends reflection support to functions. reflect_functions = ["bevy_reflect", "bevy_reflect/functions"] -## Use the configurable global error handler as the default error handler. -## -## This is typically used to turn panics from the ECS into loggable errors. -## This may be useful for production builds, -## but can result in a measurable performance impact, especially for commands. -configurable_error_handler = [] - ## Enables automatic backtrace capturing in BevyError backtrace = ["std"] @@ -47,7 +35,7 @@ backtrace = ["std"] ## Enables `tracing` integration, allowing spans and other metrics to be reported ## through that framework. -trace = ["std", "dep:tracing"] +trace = ["std", "dep:tracing", "bevy_utils/debug"] ## Enables a more detailed set of traces which may be noisy if left on by default. detailed_trace = ["trace"] @@ -74,10 +62,10 @@ async_executor = ["std", "bevy_tasks/async_executor"] std = [ "bevy_reflect?/std", "bevy_tasks/std", + "bevy_utils/parallel", "bevy_utils/std", "bitflags/std", "concurrent-queue/std", - "disqualified/alloc", "fixedbitset/std", "indexmap/std", "serde?/std", @@ -95,22 +83,21 @@ critical-section = [ "bevy_reflect?/critical-section", ] +hotpatching = ["dep:subsecond"] + [dependencies] -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "smallvec", ], default-features = false, optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ - "alloc", -] } -bevy_ecs_macros = { path = "macros", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_ecs_macros = { path = "macros", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } bitflags = { version = "2.3", default-features = false } -disqualified = { version = "1.0", default-features = false } fixedbitset = { version = "0.5", default-features = false } serde = { version = "1", default-features = false, features = [ "alloc", @@ -131,6 +118,7 @@ variadics_please = { version = "1.1", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } bumpalo = "3" +subsecond = { version = "0.7.0-alpha.1", optional = true } concurrent-queue = { version = "2.5.0", default-features = false } [target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies] diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index c2fdc53d05..b085a79c21 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -277,26 +277,24 @@ world.spawn(PlayerBundle { }); ``` -### Events +### Buffered Events -Events offer a communication channel between one or more systems. Events can be sent using the system parameter `EventWriter` and received with `EventReader`. +Buffered events offer a communication channel between one or more systems. +They can be sent using the `EventWriter` system parameter and received with `EventReader`. ```rust use bevy_ecs::prelude::*; -#[derive(Event)] -struct MyEvent { - message: String, +#[derive(Event, BufferedEvent)] +struct Message(String); + +fn writer(mut writer: EventWriter) { + writer.write(Message("Hello!".to_string())); } -fn writer(mut writer: EventWriter) { - writer.write(MyEvent { - message: "hello!".to_string(), - }); -} - -fn reader(mut reader: EventReader) { - for event in reader.read() { +fn reader(mut reader: EventReader) { + for Message(message) in reader.read() { + println!("{}", message); } } ``` @@ -309,37 +307,39 @@ Observers are systems that listen for a "trigger" of a specific `Event`: use bevy_ecs::prelude::*; #[derive(Event)] -struct MyEvent { +struct Speak { message: String } let mut world = World::new(); -world.add_observer(|trigger: Trigger| { - println!("{}", trigger.event().message); +world.add_observer(|trigger: On| { + println!("{}", trigger.message); }); world.flush(); -world.trigger(MyEvent { - message: "hello!".to_string(), +world.trigger(Speak { + message: "Hello!".to_string(), }); ``` -These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time! +These differ from `EventReader` and `EventWriter` in that they are "reactive". +Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. +Triggers can trigger other triggers, and they all will be evaluated at the same time! -Events can also be triggered to target specific entities: +If the event is an `EntityEvent`, it can also be triggered to target specific entities: ```rust use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, EntityEvent)] struct Explode; let mut world = World::new(); let entity = world.spawn_empty().id(); -world.add_observer(|trigger: Trigger, mut commands: Commands| { +world.add_observer(|trigger: On, mut commands: Commands| { println!("Entity {} goes BOOM!", trigger.target()); commands.entity(trigger.target()).despawn(); }); @@ -349,4 +349,4 @@ world.flush(); world.trigger_targets(Explode, entity); ``` -[bevy]: https://bevyengine.org/ +[bevy]: https://bevy.org/ diff --git a/crates/bevy_ecs/compile_fail/Cargo.toml b/crates/bevy_ecs/compile_fail/Cargo.toml index 48e3857f53..96c48ac6a3 100644 --- a/crates/bevy_ecs/compile_fail/Cargo.toml +++ b/crates/bevy_ecs/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_ecs_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's entity component system" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" publish = false diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs index 4076819ee3..79c2158644 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs @@ -60,4 +60,4 @@ mod case4 { pub struct BarTargetOf(Entity); } -fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {} +fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {} diff --git a/crates/bevy_ecs/examples/change_detection.rs b/crates/bevy_ecs/examples/change_detection.rs index 820860070c..42611e57e1 100644 --- a/crates/bevy_ecs/examples/change_detection.rs +++ b/crates/bevy_ecs/examples/change_detection.rs @@ -84,7 +84,7 @@ fn print_changed_entities( entity_with_mutated_component: Query<(Entity, &Age), Changed>, ) { for entity in &entity_with_added_component { - println!(" {entity} has it's first birthday!"); + println!(" {entity} has its first birthday!"); } for (entity, value) in &entity_with_mutated_component { println!(" {entity} is now {value:?} frames old"); diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index fb01184048..ecdcb31a33 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -1,4 +1,4 @@ -//! In this example a system sends a custom event with a 50/50 chance during any frame. +//! In this example a system sends a custom buffered event with a 50/50 chance during any frame. //! If an event was sent, it will be printed by the console in a receiving system. #![expect(clippy::print_stdout, reason = "Allowed in examples.")] @@ -15,7 +15,7 @@ fn main() { // Create a schedule to store our systems let mut schedule = Schedule::default(); - // Events need to be updated in every frame in order to clear our buffers. + // Buffered events need to be updated every frame in order to clear our buffers. // This update should happen before we use the events. // Here, we use system sets to control the ordering. #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] @@ -37,7 +37,7 @@ fn main() { } // This is our event that we will send and receive in systems -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct MyEvent { pub message: String, pub random_value: f32, diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index 28605a5d67..4f15bd59fd 100644 --- a/crates/bevy_ecs/macros/Cargo.toml +++ b/crates/bevy_ecs/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ecs_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" description = "Bevy ECS Macros" edition = "2024" license = "MIT OR Apache-2.0" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = { version = "2.0.99", features = ["full", "extra-traits"] } quote = "1.0" diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index e87a4b650b..ef7fad99f4 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -13,11 +13,28 @@ use syn::{ LitStr, Member, Path, Result, Token, Type, Visibility, }; -pub const EVENT: &str = "event"; +pub const EVENT: &str = "entity_event"; pub const AUTO_PROPAGATE: &str = "auto_propagate"; pub const TRAVERSAL: &str = "traversal"; pub fn derive_event(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} + }) +} + +pub fn derive_entity_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let mut auto_propagate = false; let mut traversal: Type = parse_quote!(()); @@ -38,7 +55,7 @@ pub fn derive_event(input: TokenStream) -> TokenStream { traversal = meta.value()?.parse()?; Ok(()) } - Some(ident) => Err(meta.error(format!("unsupported attribute: {}", ident))), + Some(ident) => Err(meta.error(format!("unsupported attribute: {ident}"))), None => Err(meta.error("expected identifier")), }) { return e.to_compile_error().into(); @@ -49,13 +66,30 @@ pub fn derive_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { + impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { type Traversal = #traversal; const AUTO_PROPAGATE: bool = #auto_propagate; } }) } +pub fn derive_buffered_event(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::event::BufferedEvent for #struct_name #type_generics #where_clause {} + }) +} + pub fn derive_resource(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -434,7 +468,7 @@ impl HookAttributeKind { HookAttributeKind::Path(path) => path.to_token_stream(), HookAttributeKind::Call(call) => { quote!({ - fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) { + fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) { (#call)(world, ctx) } _internal_hook @@ -658,7 +692,7 @@ fn hook_register_function_call( ) -> Option { function.map(|meta| { quote! { - fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> { + fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> { ::core::option::Option::Some(#meta) } } @@ -758,6 +792,11 @@ fn derive_relationship( #relationship_member: entity } } + + #[inline] + fn set_risky(&mut self, entity: Entity) { + self.#relationship_member = entity; + } } })) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 410317a275..7750f97259 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -1,4 +1,5 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +//! Macros for deriving ECS traits. + #![cfg_attr(docsrs, feature(doc_auto_cfg))] extern crate proc_macro; @@ -6,7 +7,6 @@ extern crate proc_macro; mod component; mod query_data; mod query_filter; -mod states; mod world_query; use crate::{ @@ -16,7 +16,7 @@ use crate::{ use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest}; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{ parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, ConstParam, Data, DataStruct, DeriveInput, GenericParam, Index, TypeParam, @@ -29,13 +29,49 @@ enum BundleFieldKind { const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; +const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components"; +#[derive(Debug)] +struct BundleAttributes { + impl_from_components: bool, +} + +impl Default for BundleAttributes { + fn default() -> Self { + Self { + impl_from_components: true, + } + } +} + +/// Implement the `Bundle` trait. #[proc_macro_derive(Bundle, attributes(bundle))] pub fn derive_bundle(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); - let named_fields = match get_struct_fields(&ast.data) { + let mut errors = vec![]; + + let mut attributes = BundleAttributes::default(); + + for attr in &ast.attrs { + if attr.path().is_ident(BUNDLE_ATTRIBUTE_NAME) { + let parsing = attr.parse_nested_meta(|meta| { + if meta.path.is_ident(BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS) { + attributes.impl_from_components = false; + return Ok(()); + } + + Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`"))) + }); + + if let Err(error) = parsing { + errors.push(error.into_compile_error()); + } + } + } + + let named_fields = match get_struct_fields(&ast.data, "derive(Bundle)") { Ok(fields) => fields, Err(e) => return e.into_compile_error().into(), }; @@ -43,6 +79,8 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let mut field_kind = Vec::with_capacity(named_fields.len()); for field in named_fields { + let mut kind = BundleFieldKind::Component; + for attr in field .attrs .iter() @@ -50,7 +88,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { { if let Err(error) = attr.parse_nested_meta(|meta| { if meta.path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) { - field_kind.push(BundleFieldKind::Ignore); + kind = BundleFieldKind::Ignore; Ok(()) } else { Err(meta.error(format!( @@ -62,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - field_kind.push(BundleFieldKind::Component); + field_kind.push(kind); } let field = named_fields @@ -75,61 +113,33 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { .map(|field| &field.ty) .collect::>(); - let mut field_component_ids = Vec::new(); - let mut field_get_component_ids = Vec::new(); - let mut field_get_components = Vec::new(); - let mut field_from_components = Vec::new(); - let mut field_required_components = Vec::new(); + let mut active_field_types = Vec::new(); + let mut active_field_tokens = Vec::new(); + let mut inactive_field_tokens = Vec::new(); for (((i, field_type), field_kind), field) in field_type .iter() .enumerate() .zip(field_kind.iter()) .zip(field.iter()) { + let field_tokens = match field { + Some(field) => field.to_token_stream(), + None => Index::from(i).to_token_stream(), + }; match field_kind { BundleFieldKind::Component => { - field_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, &mut *ids); - }); - field_required_components.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::register_required_components(components, required_components); - }); - field_get_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids); - }); - match field { - Some(field) => { - field_get_components.push(quote! { - self.#field.get_components(&mut *func); - }); - field_from_components.push(quote! { - #field: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - None => { - let index = Index::from(i); - field_get_components.push(quote! { - self.#index.get_components(&mut *func); - }); - field_from_components.push(quote! { - #index: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - } + active_field_types.push(field_type); + active_field_tokens.push(field_tokens); } - BundleFieldKind::Ignore => { - field_from_components.push(quote! { - #field: ::core::default::Default::default(), - }); - } + BundleFieldKind::Ignore => inactive_field_tokens.push(field_tokens), } } let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let struct_name = &ast.ident; - TokenStream::from(quote! { + let bundle_impl = quote! { // SAFETY: // - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order // - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass @@ -139,40 +149,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { fn component_ids( components: &mut #ecs_path::component::ComponentsRegistrator, ids: &mut impl FnMut(#ecs_path::component::ComponentId) - ){ - #(#field_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)* } fn get_component_ids( components: &#ecs_path::component::Components, ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>) - ){ - #(#field_get_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)* } fn register_required_components( components: &mut #ecs_path::component::ComponentsRegistrator, required_components: &mut #ecs_path::component::RequiredComponents - ){ - #(#field_required_components)* - } - } - - // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order - #[allow(deprecated)] - unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { - #[allow(unused_variables, non_snake_case)] - unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self - where - __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> - { - Self{ - #(#field_from_components)* - } + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)* } } + }; + let dynamic_bundle_impl = quote! { #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { type Effect = (); @@ -182,22 +179,52 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { self, func: &mut impl FnMut(#ecs_path::component::StorageType, #ecs_path::ptr::OwningPtr<'_>) ) { - #(#field_get_components)* + #(<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(self.#active_field_tokens, &mut *func);)* } } + }; + + let from_components_impl = attributes.impl_from_components.then(|| quote! { + // SAFETY: + // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order + #[allow(deprecated)] + unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { + #[allow(unused_variables, non_snake_case)] + unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self + where + __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> + { + Self { + #(#active_field_tokens: <#active_field_types as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),)* + #(#inactive_field_tokens: ::core::default::Default::default(),)* + } + } + } + }); + + let attribute_errors = &errors; + + TokenStream::from(quote! { + #(#attribute_errors)* + #bundle_impl + #from_components_impl + #dynamic_bundle_impl }) } +/// Implement the `MapEntities` trait. #[proc_macro_derive(MapEntities, attributes(entities))] pub fn derive_map_entities(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); + let map_entities_impl = map_entities( &ast.data, Ident::new("self", Span::call_site()), false, false, ); + let struct_name = &ast.ident; let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { @@ -241,7 +268,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { .as_ref() .map(|f| quote! { #f }) .unwrap_or_else(|| quote! { #i }); - field_names.push(format!("::{}", field_value)); + field_names.push(format!("::{field_value}")); fields.push(field_value); field_types.push(&field.ty); let mut field_message = None; @@ -393,10 +420,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { > #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*> #where_clause { - fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State { + fn build(self, world: &mut #path::world::World) -> <#generic_struct as #path::system::SystemParam>::State { let #builder_name { #(#fields: #field_locals,)* } = self; #state_struct_name { - state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta) + state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world) } } } @@ -425,15 +452,14 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { type State = #state_struct_name<#punctuated_generic_idents>; type Item<'w, 's> = #struct_name #ty_generics; - fn init_state(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self::State { + fn init_state(world: &mut #path::world::World) -> Self::State { #state_struct_name { - state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world, system_meta), + state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world), } } - unsafe fn new_archetype(state: &mut Self::State, archetype: &#path::archetype::Archetype, system_meta: &mut #path::system::SystemMeta) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::new_archetype(&mut state.state, archetype, system_meta) } + fn init_access(state: &Self::State, system_meta: &mut #path::system::SystemMeta, component_access_set: &mut #path::query::FilteredAccessSet<#path::component::ComponentId>, world: &mut #path::world::World) { + <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_access(&state.state, system_meta, component_access_set, world); } fn apply(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { @@ -446,7 +472,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { #[inline] unsafe fn validate_param<'w, 's>( - state: &'s Self::State, + state: &'s mut Self::State, _system_meta: &#path::system::SystemMeta, _world: #path::world::unsafe_world_cell::UnsafeWorldCell<'w>, ) -> Result<(), #path::system::SystemParamValidationError> { @@ -504,12 +530,10 @@ pub fn derive_schedule_label(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let mut trait_path = bevy_ecs_path(); trait_path.segments.push(format_ident!("schedule").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path .segments .push(format_ident!("ScheduleLabel").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "ScheduleLabel", &trait_path, &dyn_eq_path) + derive_label(input, "ScheduleLabel", &trait_path) } /// Derive macro generating an impl of the trait `SystemSet`. @@ -520,26 +544,39 @@ pub fn derive_system_set(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let mut trait_path = bevy_ecs_path(); trait_path.segments.push(format_ident!("schedule").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path.segments.push(format_ident!("SystemSet").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "SystemSet", &trait_path, &dyn_eq_path) + derive_label(input, "SystemSet", &trait_path) } pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::shared().get_path("bevy_ecs") } -#[proc_macro_derive(Event, attributes(event))] +/// Implement the `Event` trait. +#[proc_macro_derive(Event)] pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } +/// Implement the `EntityEvent` trait. +#[proc_macro_derive(EntityEvent, attributes(entity_event))] +pub fn derive_entity_event(input: TokenStream) -> TokenStream { + component::derive_entity_event(input) +} + +/// Implement the `BufferedEvent` trait. +#[proc_macro_derive(BufferedEvent)] +pub fn derive_buffered_event(input: TokenStream) -> TokenStream { + component::derive_buffered_event(input) +} + +/// Implement the `Resource` trait. #[proc_macro_derive(Resource)] pub fn derive_resource(input: TokenStream) -> TokenStream { component::derive_resource(input) } +/// Implement the `Component` trait. #[proc_macro_derive( Component, attributes(component, require, relationship, relationship_target, entities) @@ -548,16 +585,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } -#[proc_macro_derive(States)] -pub fn derive_states(input: TokenStream) -> TokenStream { - states::derive_states(input) -} - -#[proc_macro_derive(SubStates, attributes(source))] -pub fn derive_substates(input: TokenStream) -> TokenStream { - states::derive_substates(input) -} - +/// Implement the `FromWorld` trait. #[proc_macro_derive(FromWorld, attributes(from_world))] pub fn derive_from_world(input: TokenStream) -> TokenStream { let bevy_ecs_path = bevy_ecs_path(); diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 4e4529e631..12d9c2bf1c 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -74,12 +74,23 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let user_generics = ast.generics.clone(); let (user_impl_generics, user_ty_generics, user_where_clauses) = user_generics.split_for_impl(); let user_generics_with_world = { - let mut generics = ast.generics; + let mut generics = ast.generics.clone(); generics.params.insert(0, parse_quote!('__w)); generics }; let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) = user_generics_with_world.split_for_impl(); + let user_generics_with_world_and_state = { + let mut generics = ast.generics; + generics.params.insert(0, parse_quote!('__w)); + generics.params.insert(1, parse_quote!('__s)); + generics + }; + let ( + user_impl_generics_with_world_and_state, + user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, + ) = user_generics_with_world_and_state.split_for_impl(); let struct_name = ast.ident; let read_only_struct_name = if attributes.is_mutable { @@ -164,13 +175,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { &visibility, &item_struct_name, &field_types, - &user_impl_generics_with_world, + &user_impl_generics_with_world_and_state, &field_attrs, &field_visibilities, &field_idents, &user_ty_generics, - &user_ty_generics_with_world, - user_where_clauses_with_world, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, ); let mutable_world_query_impl = world_query_impl( &path, @@ -199,13 +210,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { &visibility, &read_only_item_struct_name, &read_only_field_types, - &user_impl_generics_with_world, + &user_impl_generics_with_world_and_state, &field_attrs, &field_visibilities, &field_idents, &user_ty_generics, - &user_ty_generics_with_world, - user_where_clauses_with_world, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, ); let readonly_world_query_impl = world_query_impl( &path, @@ -256,11 +267,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { for #read_only_struct_name #user_ty_generics #user_where_clauses { const IS_READ_ONLY: bool = true; type ReadOnly = #read_only_struct_name #user_ty_generics; - type Item<'__w> = #read_only_item_struct_name #user_ty_generics_with_world; + type Item<'__w, '__s> = #read_only_item_struct_name #user_ty_generics_with_world_and_state; - fn shrink<'__wlong: '__wshort, '__wshort>( - item: Self::Item<'__wlong> - ) -> Self::Item<'__wshort> { + fn shrink<'__wlong: '__wshort, '__wshort, '__s>( + item: Self::Item<'__wlong, '__s> + ) -> Self::Item<'__wshort, '__s> { #read_only_item_struct_name { #( #field_idents: <#read_only_field_types>::shrink(item.#field_idents), @@ -278,13 +289,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] - unsafe fn fetch<'__w>( + unsafe fn fetch<'__w, '__s>( + _state: &'__s Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, - ) -> Self::Item<'__w> { + ) -> Self::Item<'__w, '__s> { Self::Item { - #(#field_idents: <#read_only_field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)* + #(#field_idents: <#read_only_field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)* + } + } + } + + impl #user_impl_generics #path::query::ReleaseStateQueryData + for #read_only_struct_name #user_ty_generics #user_where_clauses + // Make these HRTBs with an unused lifetime parameter to allow trivial constraints + // See https://github.com/rust-lang/rust/issues/48214 + where #(for<'__a> #field_types: #path::query::QueryData,)* { + fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> { + Self::Item { + #(#field_idents: <#read_only_field_types>::release_state(_item.#field_idents),)* } } } @@ -301,11 +325,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { for #struct_name #user_ty_generics #user_where_clauses { const IS_READ_ONLY: bool = #is_read_only; type ReadOnly = #read_only_struct_name #user_ty_generics; - type Item<'__w> = #item_struct_name #user_ty_generics_with_world; + type Item<'__w, '__s> = #item_struct_name #user_ty_generics_with_world_and_state; - fn shrink<'__wlong: '__wshort, '__wshort>( - item: Self::Item<'__wlong> - ) -> Self::Item<'__wshort> { + fn shrink<'__wlong: '__wshort, '__wshort, '__s>( + item: Self::Item<'__wlong, '__s> + ) -> Self::Item<'__wshort, '__s> { #item_struct_name { #( #field_idents: <#field_types>::shrink(item.#field_idents), @@ -323,13 +347,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] - unsafe fn fetch<'__w>( + unsafe fn fetch<'__w, '__s>( + _state: &'__s Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, - ) -> Self::Item<'__w> { + ) -> Self::Item<'__w, '__s> { Self::Item { - #(#field_idents: <#field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)* + #(#field_idents: <#field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)* + } + } + } + + impl #user_impl_generics #path::query::ReleaseStateQueryData + for #struct_name #user_ty_generics #user_where_clauses + // Make these HRTBs with an unused lifetime parameter to allow trivial constraints + // See https://github.com/rust-lang/rust/issues/48214 + where #(for<'__a> #field_types: #path::query::ReleaseStateQueryData,)* { + fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> { + Self::Item { + #(#field_idents: <#field_types>::release_state(_item.#field_idents),)* } } } diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs index c7ddb9cc83..5ae2d2325f 100644 --- a/crates/bevy_ecs/macros/src/query_filter.rs +++ b/crates/bevy_ecs/macros/src/query_filter.rs @@ -102,11 +102,12 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { #[allow(unused_variables)] #[inline(always)] unsafe fn filter_fetch<'__w>( + _state: &Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, ) -> bool { - true #(&& <#field_types>::filter_fetch(&mut _fetch.#named_field_idents, _entity, _table_row))* + true #(&& <#field_types>::filter_fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row))* } } }; diff --git a/crates/bevy_ecs/macros/src/states.rs b/crates/bevy_ecs/macros/src/states.rs deleted file mode 100644 index ff69812aea..0000000000 --- a/crates/bevy_ecs/macros/src/states.rs +++ /dev/null @@ -1,144 +0,0 @@ -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; - -use crate::bevy_ecs_path; - -pub fn derive_states(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - let generics = ast.generics; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let mut base_trait_path = bevy_ecs_path(); - base_trait_path - .segments - .push(format_ident!("schedule").into()); - - let mut trait_path = base_trait_path.clone(); - trait_path.segments.push(format_ident!("States").into()); - - let mut state_mutation_trait_path = base_trait_path.clone(); - state_mutation_trait_path - .segments - .push(format_ident!("FreelyMutableState").into()); - - let struct_name = &ast.ident; - - quote! { - impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} - - impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { - } - } - .into() -} - -struct Source { - source_type: Path, - source_value: Pat, -} - -fn parse_sources_attr(ast: &DeriveInput) -> Result { - let mut result = ast - .attrs - .iter() - .filter(|a| a.path().is_ident("source")) - .map(|meta| { - let mut source = None; - let value = meta.parse_nested_meta(|nested| { - let source_type = nested.path.clone(); - let source_value = Pat::parse_multi(nested.value()?)?; - source = Some(Source { - source_type, - source_value, - }); - Ok(()) - }); - match source { - Some(value) => Ok(value), - None => match value { - Ok(_) => Err(syn::Error::new( - ast.span(), - "Couldn't parse SubStates source", - )), - Err(e) => Err(e), - }, - } - }) - .collect::>>()?; - - if result.len() > 1 { - return Err(syn::Error::new( - ast.span(), - "Only one source is allowed for SubStates", - )); - } - - let Some(result) = result.pop() else { - return Err(syn::Error::new(ast.span(), "SubStates require a source")); - }; - - Ok(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 generics = ast.generics; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let mut base_trait_path = bevy_ecs_path(); - base_trait_path - .segments - .push(format_ident!("schedule").into()); - - let mut trait_path = base_trait_path.clone(); - trait_path.segments.push(format_ident!("SubStates").into()); - - let mut state_set_trait_path = base_trait_path.clone(); - state_set_trait_path - .segments - .push(format_ident!("StateSet").into()); - - let mut state_trait_path = base_trait_path.clone(); - state_trait_path - .segments - .push(format_ident!("States").into()); - - let mut state_mutation_trait_path = base_trait_path.clone(); - state_mutation_trait_path - .segments - .push(format_ident!("FreelyMutableState").into()); - - let struct_name = &ast.ident; - - let source_state_type = sources.source_type; - let source_state_value = sources.source_value; - - let result = quote! { - impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { - type SourceStates = #source_state_type; - - fn should_exist(sources: #source_state_type) -> Option { - if matches!(sources, #source_state_value) { - Some(Self::default()) - } else { - None - } - } - } - - impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { - const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; - } - - impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { - } - }; - - // panic!("Got Result\n{}", result.to_string()); - - result.into() -} diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 77ee532a50..5a7d164b80 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -10,13 +10,13 @@ pub(crate) fn item_struct( visibility: &Visibility, item_struct_name: &Ident, field_types: &Vec, - user_impl_generics_with_world: &ImplGenerics, + user_impl_generics_with_world_and_state: &ImplGenerics, field_attrs: &Vec>, field_visibilities: &Vec, field_idents: &Vec, user_ty_generics: &TypeGenerics, - user_ty_generics_with_world: &TypeGenerics, - user_where_clauses_with_world: Option<&WhereClause>, + user_ty_generics_with_world_and_state: &TypeGenerics, + user_where_clauses_with_world_and_state: Option<&WhereClause>, ) -> proc_macro2::TokenStream { let item_attrs = quote! { #[doc = concat!( @@ -33,20 +33,20 @@ pub(crate) fn item_struct( Fields::Named(_) => quote! { #derive_macro_call #item_attrs - #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { - #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w>,)* + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state { + #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w, '__s>,)* } }, Fields::Unnamed(_) => quote! { #derive_macro_call #item_attrs - #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world( - #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w>, )* + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state( + #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w, '__s>, )* ); }, Fields::Unit => quote! { #item_attrs - #visibility type #item_struct_name #user_ty_generics_with_world = #struct_name #user_ty_generics; + #visibility type #item_struct_name #user_ty_generics_with_world_and_state = #struct_name #user_ty_generics; }, } } @@ -79,7 +79,7 @@ pub(crate) fn world_query_impl( #[automatically_derived] #visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { #(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)* - #marker_name: &'__w (), + #marker_name: &'__w(), } impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world @@ -92,7 +92,7 @@ pub(crate) fn world_query_impl( } } - // SAFETY: `update_component_access` and `update_archetype_component_access` are called on every field + // SAFETY: `update_component_access` is called on every field unsafe impl #user_impl_generics #path::query::WorldQuery for #struct_name #user_ty_generics #user_where_clauses { @@ -110,9 +110,9 @@ pub(crate) fn world_query_impl( } } - unsafe fn init_fetch<'__w>( + unsafe fn init_fetch<'__w, '__s>( _world: #path::world::unsafe_world_cell::UnsafeWorldCell<'__w>, - state: &Self::State, + state: &'__s Self::State, _last_run: #path::component::Tick, _this_run: #path::component::Tick, ) -> ::Fetch<'__w> { @@ -133,9 +133,9 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_archetype` for each member that implements `Fetch` #[inline] - unsafe fn set_archetype<'__w>( + unsafe fn set_archetype<'__w, '__s>( _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + _state: &'__s Self::State, _archetype: &'__w #path::archetype::Archetype, _table: &'__w #path::storage::Table ) { @@ -144,9 +144,9 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_table` for each member that implements `Fetch` #[inline] - unsafe fn set_table<'__w>( + unsafe fn set_table<'__w, '__s>( _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + _state: &'__s Self::State, _table: &'__w #path::storage::Table ) { #(<#field_types>::set_table(&mut _fetch.#named_field_idents, &_state.#named_field_idents, _table);)* diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 2468c5e9a8..1ecbad16a1 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -23,15 +23,21 @@ use crate::{ bundle::BundleId, component::{ComponentId, Components, RequiredComponentConstructor, StorageType}, entity::{Entity, EntityLocation}, + event::Event, observer::Observers, - storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow}, + storage::{ImmutableSparseSet, SparseArray, SparseSet, TableId, TableRow}, }; use alloc::{boxed::Box, vec::Vec}; -use bevy_platform::collections::HashMap; +use bevy_platform::collections::{hash_map::Entry, HashMap}; use core::{ hash::Hash, ops::{Index, IndexMut, RangeFrom}, }; +use nonmax::NonMaxU32; + +#[derive(Event)] +#[expect(dead_code, reason = "Prepare for the upcoming Query as Entities")] +pub(crate) struct ArchetypeCreated(pub ArchetypeId); /// An opaque location within a [`Archetype`]. /// @@ -44,23 +50,30 @@ use core::{ #[derive(Debug, Copy, Clone, Eq, PartialEq)] // SAFETY: Must be repr(transparent) due to the safety requirements on EntityLocation #[repr(transparent)] -pub struct ArchetypeRow(u32); +pub struct ArchetypeRow(NonMaxU32); impl ArchetypeRow { /// Index indicating an invalid archetype row. /// This is meant to be used as a placeholder. - pub const INVALID: ArchetypeRow = ArchetypeRow(u32::MAX); + // TODO: Deprecate in favor of options, since `INVALID` is, technically, valid. + pub const INVALID: ArchetypeRow = ArchetypeRow(NonMaxU32::MAX); /// Creates a `ArchetypeRow`. #[inline] - pub const fn new(index: usize) -> Self { - Self(index as u32) + pub const fn new(index: NonMaxU32) -> Self { + Self(index) } /// Gets the index of the row. #[inline] pub const fn index(self) -> usize { - self.0 as usize + self.0.get() as usize + } + + /// Gets the index of the row. + #[inline] + pub const fn index_u32(self) -> u32 { + self.0.get() } } @@ -341,7 +354,6 @@ pub(crate) struct ArchetypeSwapRemoveResult { /// [`Component`]: crate::component::Component struct ArchetypeComponentInfo { storage_type: StorageType, - archetype_component_id: ArchetypeComponentId, } bitflags::bitflags! { @@ -386,14 +398,14 @@ impl Archetype { observers: &Observers, id: ArchetypeId, table_id: TableId, - table_components: impl Iterator, - sparse_set_components: impl Iterator, + table_components: impl Iterator, + sparse_set_components: impl Iterator, ) -> Self { let (min_table, _) = table_components.size_hint(); let (min_sparse, _) = sparse_set_components.size_hint(); let mut flags = ArchetypeFlags::empty(); let mut archetype_components = SparseSet::with_capacity(min_table + min_sparse); - for (idx, (component_id, archetype_component_id)) in table_components.enumerate() { + for (idx, component_id) in table_components.enumerate() { // SAFETY: We are creating an archetype that includes this component so it must exist let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); @@ -402,7 +414,6 @@ impl Archetype { component_id, ArchetypeComponentInfo { storage_type: StorageType::Table, - archetype_component_id, }, ); // NOTE: the `table_components` are sorted AND they were inserted in the `Table` in the same @@ -414,7 +425,7 @@ impl Archetype { .insert(id, ArchetypeRecord { column: Some(idx) }); } - for (component_id, archetype_component_id) in sparse_set_components { + for component_id in sparse_set_components { // SAFETY: We are creating an archetype that includes this component so it must exist let info = unsafe { components.get_info_unchecked(component_id) }; info.update_archetype_flags(&mut flags); @@ -423,7 +434,6 @@ impl Archetype { component_id, ArchetypeComponentInfo { storage_type: StorageType::SparseSet, - archetype_component_id, }, ); component_index @@ -467,6 +477,27 @@ impl Archetype { &self.entities } + /// Fetches the entities contained in this archetype. + #[inline] + pub fn entities_with_location(&self) -> impl Iterator { + self.entities.iter().enumerate().map( + |(archetype_row, &ArchetypeEntity { entity, table_row })| { + ( + entity, + EntityLocation { + archetype_id: self.id, + // SAFETY: The entities in the archetype must be unique and there are never more than u32::MAX entities. + archetype_row: unsafe { + ArchetypeRow::new(NonMaxU32::new_unchecked(archetype_row as u32)) + }, + table_id: self.table_id, + table_row, + }, + ) + }, + ) + } + /// Gets an iterator of all of the components stored in [`Table`]s. /// /// All of the IDs are unique. @@ -507,16 +538,6 @@ impl Archetype { self.components.len() } - /// Gets an iterator of all of the components in the archetype, along with - /// their archetype component ID. - pub(crate) fn components_with_archetype_component_id( - &self, - ) -> impl Iterator + '_ { - self.components - .iter() - .map(|(component_id, info)| (*component_id, info.archetype_component_id)) - } - /// Fetches an immutable reference to the archetype's [`Edges`], a cache of /// archetypal relationships. #[inline] @@ -569,7 +590,8 @@ impl Archetype { entity: Entity, table_row: TableRow, ) -> EntityLocation { - let archetype_row = ArchetypeRow::new(self.entities.len()); + // SAFETY: An entity can not have multiple archetype rows and there can not be more than u32::MAX entities. + let archetype_row = unsafe { ArchetypeRow::new(NonMaxU32::new_unchecked(self.len())) }; self.entities.push(ArchetypeEntity { entity, table_row }); EntityLocation { @@ -606,8 +628,10 @@ impl Archetype { /// Gets the total number of entities that belong to the archetype. #[inline] - pub fn len(&self) -> usize { - self.entities.len() + pub fn len(&self) -> u32 { + // No entity may have more than one archetype row, so there are no duplicates, + // and there may only ever be u32::MAX entities, so the length never exceeds u32's cappacity. + self.entities.len() as u32 } /// Checks if the archetype has any entities. @@ -632,19 +656,6 @@ impl Archetype { .map(|info| info.storage_type) } - /// Fetches the corresponding [`ArchetypeComponentId`] for a component in the archetype. - /// Returns `None` if the component is not part of the archetype. - /// This runs in `O(1)` time. - #[inline] - pub fn get_archetype_component_id( - &self, - component_id: ComponentId, - ) -> Option { - self.components - .get(component_id) - .map(|info| info.archetype_component_id) - } - /// Clears all entities from the archetype. pub(crate) fn clear_entities(&mut self) { self.entities.clear(); @@ -680,41 +691,41 @@ impl Archetype { self.flags().contains(ArchetypeFlags::ON_DESPAWN_HOOK) } - /// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer + /// Returns true if any of the components in this archetype have at least one [`Add`] observer /// - /// [`OnAdd`]: crate::world::OnAdd + /// [`Add`]: crate::lifecycle::Add #[inline] pub fn has_add_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer + /// Returns true if any of the components in this archetype have at least one [`Insert`] observer /// - /// [`OnInsert`]: crate::world::OnInsert + /// [`Insert`]: crate::lifecycle::Insert #[inline] pub fn has_insert_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer + /// Returns true if any of the components in this archetype have at least one [`Replace`] observer /// - /// [`OnReplace`]: crate::world::OnReplace + /// [`Replace`]: crate::lifecycle::Replace #[inline] pub fn has_replace_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer + /// Returns true if any of the components in this archetype have at least one [`Remove`] observer /// - /// [`OnRemove`]: crate::world::OnRemove + /// [`Remove`]: crate::lifecycle::Remove #[inline] pub fn has_remove_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer + /// Returns true if any of the components in this archetype have at least one [`Despawn`] observer /// - /// [`OnDespawn`]: crate::world::OnDespawn + /// [`Despawn`]: crate::lifecycle::Despawn #[inline] pub fn has_despawn_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER) @@ -742,46 +753,6 @@ struct ArchetypeComponents { sparse_set_components: Box<[ComponentId]>, } -/// An opaque unique joint ID for a [`Component`] in an [`Archetype`] within a [`World`]. -/// -/// A component may be present within multiple archetypes, but each component within -/// each archetype has its own unique `ArchetypeComponentId`. This is leveraged by the system -/// schedulers to opportunistically run multiple systems in parallel that would otherwise -/// conflict. For example, `Query<&mut A, With>` and `Query<&mut A, Without>` can run in -/// parallel as the matched `ArchetypeComponentId` sets for both queries are disjoint, even -/// though `&mut A` on both queries point to the same [`ComponentId`]. -/// -/// In SQL terms, these IDs are composite keys on a [many-to-many relationship] between archetypes -/// and components. Each component type will have only one [`ComponentId`], but may have many -/// [`ArchetypeComponentId`]s, one for every archetype the component is present in. Likewise, each -/// archetype will have only one [`ArchetypeId`] but may have many [`ArchetypeComponentId`]s, one -/// for each component that belongs to the archetype. -/// -/// Every [`Resource`] is also assigned one of these IDs. As resources do not belong to any -/// particular archetype, a resource's ID uniquely identifies it. -/// -/// These IDs are only valid within a given World, and are not globally unique. -/// Attempting to use an ID on a world that it wasn't sourced from will -/// not point to the same archetype nor the same component. -/// -/// [`Component`]: crate::component::Component -/// [`World`]: crate::world::World -/// [`Resource`]: crate::resource::Resource -/// [many-to-many relationship]: https://en.wikipedia.org/wiki/Many-to-many_(data_model) -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct ArchetypeComponentId(usize); - -impl SparseSetIndex for ArchetypeComponentId { - #[inline] - fn sparse_set_index(&self) -> usize { - self.0 - } - - fn get_sparse_set_index(value: usize) -> Self { - Self(value) - } -} - /// Maps a [`ComponentId`] to the list of [`Archetypes`]([`Archetype`]) that contain the [`Component`](crate::component::Component), /// along with an [`ArchetypeRecord`] which contains some metadata about how the component is stored in the archetype. pub type ComponentIndex = HashMap>; @@ -794,7 +765,6 @@ pub type ComponentIndex = HashMap, - archetype_component_count: usize, /// find the archetype id by the archetype's components by_components: HashMap, /// find all the archetypes that contain a component @@ -818,7 +788,6 @@ impl Archetypes { archetypes: Vec::new(), by_components: Default::default(), by_component: Default::default(), - archetype_component_count: 0, }; // SAFETY: Empty archetype has no components unsafe { @@ -873,22 +842,6 @@ impl Archetypes { } } - /// Generate and store a new [`ArchetypeComponentId`]. - /// - /// This simply increment the counter and return the new value. - /// - /// # Panics - /// - /// On archetype component id overflow. - pub(crate) fn new_archetype_component_id(&mut self) -> ArchetypeComponentId { - let id = ArchetypeComponentId(self.archetype_component_count); - self.archetype_component_count = self - .archetype_component_count - .checked_add(1) - .expect("archetype_component_count overflow"); - id - } - /// Fetches an immutable reference to an [`Archetype`] using its /// ID. Returns `None` if no corresponding archetype exists. #[inline] @@ -921,6 +874,10 @@ impl Archetypes { } /// Gets the archetype id matching the given inputs or inserts a new one if it doesn't exist. + /// + /// Specifically, it returns a tuple where the first element + /// is the [`ArchetypeId`] that the given inputs belong to, and the second element is a boolean indicating whether a new archetype was created. + /// /// `table_components` and `sparse_set_components` must be sorted /// /// # Safety @@ -933,56 +890,35 @@ impl Archetypes { table_id: TableId, table_components: Vec, sparse_set_components: Vec, - ) -> ArchetypeId { + ) -> (ArchetypeId, bool) { let archetype_identity = ArchetypeComponents { sparse_set_components: sparse_set_components.into_boxed_slice(), table_components: table_components.into_boxed_slice(), }; let archetypes = &mut self.archetypes; - let archetype_component_count = &mut self.archetype_component_count; let component_index = &mut self.by_component; - *self - .by_components - .entry(archetype_identity) - .or_insert_with_key(move |identity| { + match self.by_components.entry(archetype_identity) { + Entry::Occupied(occupied) => (*occupied.get(), false), + Entry::Vacant(vacant) => { let ArchetypeComponents { table_components, sparse_set_components, - } = identity; + } = vacant.key(); let id = ArchetypeId::new(archetypes.len()); - let table_start = *archetype_component_count; - *archetype_component_count += table_components.len(); - let table_archetype_components = - (table_start..*archetype_component_count).map(ArchetypeComponentId); - let sparse_start = *archetype_component_count; - *archetype_component_count += sparse_set_components.len(); - let sparse_set_archetype_components = - (sparse_start..*archetype_component_count).map(ArchetypeComponentId); archetypes.push(Archetype::new( components, component_index, observers, id, table_id, - table_components - .iter() - .copied() - .zip(table_archetype_components), - sparse_set_components - .iter() - .copied() - .zip(sparse_set_archetype_components), + table_components.iter().copied(), + sparse_set_components.iter().copied(), )); - id - }) - } - - /// Returns the number of components that are stored in archetypes. - /// Note that if some component `T` is stored in more than one archetype, it will be counted once for each archetype it's present in. - #[inline] - pub fn archetype_components_len(&self) -> usize { - self.archetype_component_count + vacant.insert(id); + (id, true) + } + } } /// Clears all entities from all archetypes. @@ -1024,6 +960,7 @@ impl Index> for Archetypes { &self.archetypes[index.start.0.index()..] } } + impl Index for Archetypes { type Output = Archetype; diff --git a/crates/bevy_ecs/src/batching.rs b/crates/bevy_ecs/src/batching.rs index cdffbfa05d..ab9f2d582c 100644 --- a/crates/bevy_ecs/src/batching.rs +++ b/crates/bevy_ecs/src/batching.rs @@ -22,7 +22,7 @@ use core::ops::Range; /// [`EventReader::par_read`]: crate::event::EventReader::par_read #[derive(Clone, Debug)] pub struct BatchingStrategy { - /// The upper and lower limits for a batch of entities. + /// The upper and lower limits for a batch of items. /// /// Setting the bounds to the same value will result in a fixed /// batch size. diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index a7fb4f6fd4..8efdc60ad9 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2,12 +2,63 @@ //! //! This module contains the [`Bundle`] trait and some other helper types. +/// Derive the [`Bundle`] trait +/// +/// You can apply this derive macro to structs that are +/// composed of [`Component`]s or +/// other [`Bundle`]s. +/// +/// ## Attributes +/// +/// Sometimes parts of the Bundle should not be inserted. +/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped. +/// In that case, the field needs to implement [`Default`] unless you also ignore +/// the [`BundleFromComponents`] implementation. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # +/// #[derive(Bundle)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// +/// #[bundle(ignore)] +/// creator: Option +/// } +/// ``` +/// +/// Some fields may be bundles that do not implement +/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted. +/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an +/// example usage. +/// In those cases you can either ignore it as above, +/// or you can opt out the whole Struct by marking it as ignored with +/// `#[bundle(ignore_from_components)]`. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # #[derive(Component)] +/// # struct Marker; +/// # +/// use bevy_ecs::spawn::SpawnRelatedBundle; +/// +/// #[derive(Bundle)] +/// #[bundle(ignore_from_components)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// related_spawner: SpawnRelatedBundle>, +/// } +/// ``` pub use bevy_ecs_macros::Bundle; use crate::{ archetype::{ - Archetype, ArchetypeAfterBundleInsert, ArchetypeId, Archetypes, BundleComponentStatus, - ComponentStatus, SpawnBundleStatus, + Archetype, ArchetypeAfterBundleInsert, ArchetypeCreated, ArchetypeId, Archetypes, + BundleComponentStatus, ComponentStatus, SpawnBundleStatus, }, change_detection::MaybeLocation, component::{ @@ -15,15 +66,13 @@ use crate::{ RequiredComponents, StorageType, Tick, }, entity::{Entities, Entity, EntityLocation}, + lifecycle::{ADD, INSERT, REMOVE, REPLACE}, observer::Observers, prelude::World, query::DebugCheckedUnwrap, relationship::RelationshipHookMode, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{ - unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE, - ON_REPLACE, - }, + world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut}, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; @@ -501,10 +550,9 @@ impl BundleInfo { // SAFETY: the caller ensures component_id is valid. unsafe { components.get_info_unchecked(id).name() } }) - .collect::>() - .join(", "); + .collect::>(); - panic!("Bundle {bundle_type_name} has duplicate components: {names}"); + panic!("Bundle {bundle_type_name} has duplicate components: {names:?}"); } // handle explicit components @@ -732,7 +780,7 @@ impl BundleInfo { } } - /// Inserts a bundle into the given archetype and returns the resulting archetype. + /// Inserts a bundle into the given archetype and returns the resulting archetype and whether a new archetype was created. /// This could be the same [`ArchetypeId`], in the event that inserting the given bundle /// does not result in an [`Archetype`] change. /// @@ -747,12 +795,12 @@ impl BundleInfo { components: &Components, observers: &Observers, archetype_id: ArchetypeId, - ) -> ArchetypeId { + ) -> (ArchetypeId, bool) { if let Some(archetype_after_insert_id) = archetypes[archetype_id] .edges() .get_archetype_after_bundle_insert(self.id) { - return archetype_after_insert_id; + return (archetype_after_insert_id, false); } let mut new_table_components = Vec::new(); let mut new_sparse_set_components = Vec::new(); @@ -806,7 +854,7 @@ impl BundleInfo { added, existing, ); - archetype_id + (archetype_id, false) } else { let table_id; let table_components; @@ -842,13 +890,14 @@ impl BundleInfo { }; }; // SAFETY: ids in self must be valid - let new_archetype_id = archetypes.get_id_or_insert( + let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( components, observers, table_id, table_components, sparse_set_components, ); + // Add an edge from the old archetype to the new archetype. archetypes[archetype_id] .edges_mut() @@ -860,11 +909,11 @@ impl BundleInfo { added, existing, ); - new_archetype_id + (new_archetype_id, is_new_created) } } - /// Removes a bundle from the given archetype and returns the resulting archetype + /// Removes a bundle from the given archetype and returns the resulting archetype and whether a new archetype was created. /// (or `None` if the removal was invalid). /// This could be the same [`ArchetypeId`], in the event that removing the given bundle /// does not result in an [`Archetype`] change. @@ -887,7 +936,7 @@ impl BundleInfo { observers: &Observers, archetype_id: ArchetypeId, intersection: bool, - ) -> Option { + ) -> (Option, bool) { // Check the archetype graph to see if the bundle has been // removed from this archetype in the past. let archetype_after_remove_result = { @@ -898,9 +947,9 @@ impl BundleInfo { edges.get_archetype_after_bundle_take(self.id()) } }; - let result = if let Some(result) = archetype_after_remove_result { + let (result, is_new_created) = if let Some(result) = archetype_after_remove_result { // This bundle removal result is cached. Just return that! - result + (result, false) } else { let mut next_table_components; let mut next_sparse_set_components; @@ -925,7 +974,7 @@ impl BundleInfo { current_archetype .edges_mut() .cache_archetype_after_bundle_take(self.id(), None); - return None; + return (None, false); } } @@ -953,14 +1002,14 @@ impl BundleInfo { }; } - let new_archetype_id = archetypes.get_id_or_insert( + let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( components, observers, next_table_id, next_table_components, next_sparse_set_components, ); - Some(new_archetype_id) + (Some(new_archetype_id), is_new_created) }; let current_archetype = &mut archetypes[archetype_id]; // Cache the result in an edge. @@ -973,7 +1022,7 @@ impl BundleInfo { .edges_mut() .cache_archetype_after_bundle_take(self.id(), result); } - result + (result, is_new_created) } } @@ -1036,14 +1085,15 @@ impl<'w> BundleInserter<'w> { // SAFETY: We will not make any accesses to the command queue, component or resource data of this world let bundle_info = world.bundles.get_unchecked(bundle_id); let bundle_id = bundle_info.id(); - let new_archetype_id = bundle_info.insert_bundle_into_archetype( + let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype( &mut world.archetypes, &mut world.storages, &world.components, &world.observers, archetype_id, ); - if new_archetype_id == archetype_id { + + let inserter = if new_archetype_id == archetype_id { let archetype = &mut world.archetypes[archetype_id]; // SAFETY: The edge is assured to be initialized when we called insert_bundle_into_archetype let archetype_after_insert = unsafe { @@ -1103,7 +1153,15 @@ impl<'w> BundleInserter<'w> { world: world.as_unsafe_world_cell(), } } + }; + + if is_new_created { + inserter + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); } + inserter } /// # Safety @@ -1132,8 +1190,8 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), archetype_after_insert.iter_existing(), caller, ); @@ -1193,16 +1251,16 @@ impl<'w> BundleInserter<'w> { unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, + }), ); } let new_location = new_archetype.allocate(entity, result.table_row); - entities.set(entity.index(), new_location); + entities.set(entity.index(), Some(new_location)); let after_effect = bundle_info.write_components( table, sparse_sets, @@ -1242,19 +1300,19 @@ impl<'w> BundleInserter<'w> { unsafe { entities.get(swapped_entity).debug_checked_unwrap() }; entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, + }), ); } // PERF: store "non bundle" components in edge, then just move those to avoid // redundant copies let move_result = table.move_to_superset_unchecked(result.table_row, new_table); let new_location = new_archetype.allocate(entity, move_result.new_row); - entities.set(entity.index(), new_location); + entities.set(entity.index(), Some(new_location)); // If an entity was moved into this entity's table spot, update its table row. if let Some(swapped_entity) = move_result.swapped_entity { @@ -1264,12 +1322,12 @@ impl<'w> BundleInserter<'w> { entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: swapped_location.archetype_row, table_id: swapped_location.table_id, table_row: result.table_row, - }, + }), ); if archetype.id() == swapped_location.archetype_id { @@ -1317,8 +1375,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, - entity, + ADD, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1335,8 +1393,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), archetype_after_insert.iter_inserted(), caller, ); @@ -1354,8 +1412,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1421,7 +1479,7 @@ impl<'w> BundleRemover<'w> { ) -> Option { let bundle_info = world.bundles.get_unchecked(bundle_id); // SAFETY: Caller ensures archetype and bundle ids are correct. - let new_archetype_id = unsafe { + let (new_archetype_id, is_new_created) = unsafe { bundle_info.remove_bundle_from_archetype( &mut world.archetypes, &mut world.storages, @@ -1429,11 +1487,14 @@ impl<'w> BundleRemover<'w> { &world.observers, archetype_id, !require_all, - )? + ) }; + let new_archetype_id = new_archetype_id?; + if new_archetype_id == archetype_id { return None; } + let (old_archetype, new_archetype) = world.archetypes.get_2_mut(archetype_id, new_archetype_id); @@ -1447,13 +1508,20 @@ impl<'w> BundleRemover<'w> { Some((old.into(), new.into())) }; - Some(Self { + let remover = Self { bundle_info: bundle_info.into(), new_archetype: new_archetype.into(), old_archetype: old_archetype.into(), old_and_new_table: tables, world: world.as_unsafe_world_cell(), - }) + }; + if is_new_created { + remover + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); + } + Some(remover) } /// This can be passed to [`remove`](Self::remove) as the `pre_remove` function if you don't want to do anything before removing. @@ -1498,8 +1566,8 @@ impl<'w> BundleRemover<'w> { }; if self.old_archetype.as_ref().has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1513,8 +1581,8 @@ impl<'w> BundleRemover<'w> { ); if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, - entity, + REMOVE, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1573,12 +1641,12 @@ impl<'w> BundleRemover<'w> { world.entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, + }), ); } @@ -1614,12 +1682,12 @@ impl<'w> BundleRemover<'w> { world.entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: swapped_location.archetype_row, table_id: swapped_location.table_id, table_row: location.table_row, - }, + }), ); world.archetypes[swapped_location.archetype_id] .set_entity_table_row(swapped_location.archetype_row, location.table_row); @@ -1635,7 +1703,7 @@ impl<'w> BundleRemover<'w> { // SAFETY: The entity is valid and has been moved to the new location already. unsafe { - world.entities.set(entity.index(), new_location); + world.entities.set(entity.index(), Some(new_location)); } (new_location, pre_remove_result) @@ -1675,22 +1743,30 @@ impl<'w> BundleSpawner<'w> { change_tick: Tick, ) -> Self { let bundle_info = world.bundles.get_unchecked(bundle_id); - let new_archetype_id = bundle_info.insert_bundle_into_archetype( + let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype( &mut world.archetypes, &mut world.storages, &world.components, &world.observers, ArchetypeId::EMPTY, ); + let archetype = &mut world.archetypes[new_archetype_id]; let table = &mut world.storages.tables[archetype.table_id()]; - Self { + let spawner = Self { bundle_info: bundle_info.into(), table: table.into(), archetype: archetype.into(), change_tick, world: world.as_unsafe_world_cell(), + }; + if is_new_created { + spawner + .world + .into_deferred() + .trigger(ArchetypeCreated(new_archetype_id)); } + spawner } #[inline] @@ -1736,7 +1812,8 @@ impl<'w> BundleSpawner<'w> { InsertMode::Replace, caller, ); - entities.set_spawn_despawn(entity.index(), location, caller, self.change_tick); + entities.set(entity.index(), Some(location)); + entities.mark_spawn_despawn(entity.index(), caller, self.change_tick); (location, after_effect) }; @@ -1755,8 +1832,8 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, - entity, + ADD, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -1770,8 +1847,8 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -2042,7 +2119,9 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { #[cfg(test)] mod tests { - use crate::{component::HookContext, prelude::*, world::DeferredWorld}; + use crate::{ + archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld, + }; use alloc::vec; #[derive(Component)] @@ -2091,6 +2170,26 @@ mod tests { } } + #[derive(Bundle)] + #[bundle(ignore_from_components)] + struct BundleNoExtract { + b: B, + no_from_comp: crate::spawn::SpawnRelatedBundle>, + } + + #[test] + fn can_spawn_bundle_without_extract() { + let mut world = World::new(); + let id = world + .spawn(BundleNoExtract { + b: B, + no_from_comp: Children::spawn(Spawn(C)), + }) + .id(); + + assert!(world.entity(id).get::().is_some()); + } + #[test] fn component_hook_order_spawn_despawn() { let mut world = World::new(); @@ -2237,6 +2336,28 @@ mod tests { assert_eq!(entity.get(), Some(&V("one"))); } + #[derive(Component, Debug, Eq, PartialEq)] + #[component(storage = "SparseSet")] + pub struct SparseV(&'static str); + + #[derive(Component, Debug, Eq, PartialEq)] + #[component(storage = "SparseSet")] + pub struct SparseA; + + #[test] + fn sparse_set_insert_if_new() { + let mut world = World::new(); + let id = world.spawn(SparseV("one")).id(); + let mut entity = world.entity_mut(id); + entity.insert_if_new(SparseV("two")); + entity.insert_if_new((SparseA, SparseV("three"))); + entity.flush(); + // should still contain "one" + let entity = world.entity(id); + assert!(entity.contains::()); + assert_eq!(entity.get(), Some(&SparseV("one"))); + } + #[test] fn sorted_remove() { let mut a = vec![1, 2, 3, 4, 5, 6, 7]; @@ -2257,4 +2378,32 @@ mod tests { assert_eq!(a, vec![1]); } + + #[test] + fn new_archetype_created() { + let mut world = World::new(); + #[derive(Resource, Default)] + struct Count(u32); + world.init_resource::(); + world.add_observer(|_t: On, mut count: ResMut| { + count.0 += 1; + }); + + let mut e = world.spawn((A, B)); + e.insert(C); + e.remove::(); + e.insert(A); + e.insert(A); + + assert_eq!(world.resource::().0, 3); + } + + #[derive(Bundle)] + #[expect(unused, reason = "tests the output of the derive macro is valid")] + struct Ignore { + #[bundle(ignore)] + foo: i32, + #[bundle(ignore)] + bar: i32, + } } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 767134fdc6..006b738caf 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -230,7 +230,7 @@ pub trait DetectChangesMut: DetectChanges { /// #[derive(Resource, PartialEq, Eq)] /// pub struct Score(u32); /// - /// #[derive(Event, PartialEq, Eq)] + /// #[derive(Event, BufferedEvent, PartialEq, Eq)] /// pub struct ScoreChanged { /// current: u32, /// previous: u32, @@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,); /// Unique mutable borrow of an entity's component or of a resource. /// -/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to -/// `&mut T`, which only provides access to change detection while in its mutable form: +/// This can be used in queries to access change detection from immutable query methods, as opposed +/// to `&mut T` which only provides access to change detection from mutable query methods. /// /// ```rust /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::query::QueryData; /// # -/// #[derive(Component, Clone)] +/// #[derive(Component, Clone, Debug)] /// struct Name(String); /// -/// #[derive(Component, Clone, Copy)] +/// #[derive(Component, Clone, Copy, Debug)] /// struct Health(f32); /// -/// #[derive(Component, Clone, Copy)] -/// struct Position { -/// x: f32, -/// y: f32, -/// }; +/// fn my_system(mut query: Query<(Mut, &mut Health)>) { +/// // Mutable access provides change detection information for both parameters: +/// // - `name` has type `Mut` +/// // - `health` has type `Mut` +/// for (name, health) in query.iter_mut() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?} (last changed: {:?})", health, health.last_changed()); +/// # println!("{}{}", name.0, health.0); // Silence dead_code warning +/// } /// -/// #[derive(Component, Clone, Copy)] -/// struct Player { -/// id: usize, -/// }; -/// -/// #[derive(QueryData)] -/// #[query_data(mutable)] -/// struct PlayerQuery { -/// id: &'static Player, -/// -/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it. -/// name: Mut<'static, Name>, -/// -/// health: &'static mut Health, -/// position: &'static mut Position, -/// } -/// -/// fn update_player_avatars(players_query: Query) { -/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`. -/// for player in players_query.iter() { -/// if player.name.is_changed() { -/// // Update the player's name. This clones a String, and so is more expensive. -/// update_player_name(player.id, player.name.clone()); -/// } -/// -/// // Update the health bar. -/// update_player_health(player.id, *player.health); -/// -/// // Update the player's position. -/// update_player_position(player.id, *player.position); +/// // Immutable access only provides change detection for `Name`: +/// // - `name` has type `Ref` +/// // - `health` has type `&Health` +/// for (name, health) in query.iter() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?}", health); /// } /// } /// -/// # bevy_ecs::system::assert_is_system(update_player_avatars); -/// -/// # fn update_player_name(player: &Player, new_name: Name) {} -/// # fn update_player_health(player: &Player, new_health: Health) {} -/// # fn update_player_position(player: &Player, new_position: Position) {} +/// # bevy_ecs::system::assert_is_system(my_system); /// ``` pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, @@ -1517,7 +1493,7 @@ impl MaybeLocation { /// within a non-tracked function body. #[inline] #[track_caller] - pub fn caller() -> Self { + pub const fn caller() -> Self { // Note that this cannot use `new_with`, since `FnOnce` invocations cannot be annotated with `#[track_caller]`. MaybeLocation { #[cfg(feature = "track_location")] @@ -1847,8 +1823,7 @@ mod tests { let mut new = value.map_unchanged(|ptr| { // SAFETY: The underlying type of `ptr` matches `reflect_from_ptr`. - let value = unsafe { reflect_from_ptr.as_reflect_mut(ptr) }; - value + unsafe { reflect_from_ptr.as_reflect_mut(ptr) } }); assert!(!new.is_changed()); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index e7c14d1c3e..831402240d 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -5,16 +5,17 @@ use crate::{ bundle::BundleInfo, change_detection::{MaybeLocation, MAX_CHANGE_AGE}, entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, ComponentHooks}, query::DebugCheckedUnwrap, - relationship::RelationshipHookMode, resource::Resource, storage::{SparseSetIndex, SparseSets, Table, TableRow}, system::{Local, SystemParam}, - world::{DeferredWorld, FromWorld, World}, + world::{FromWorld, World}, }; use alloc::boxed::Box; use alloc::{borrow::Cow, format, vec::Vec}; pub use bevy_ecs_macros::Component; +use bevy_ecs_macros::Event; use bevy_platform::sync::Arc; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -23,7 +24,7 @@ use bevy_platform::{ use bevy_ptr::{OwningPtr, UnsafeCellDeref}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -use bevy_utils::TypeIdMap; +use bevy_utils::{prelude::DebugName, TypeIdMap}; use core::{ alloc::Layout, any::{Any, TypeId}, @@ -33,7 +34,6 @@ use core::{ mem::needs_drop, ops::{Deref, DerefMut}, }; -use disqualified::ShortName; use smallvec::SmallVec; use thiserror::Error; @@ -375,7 +375,8 @@ use thiserror::Error; /// - `#[component(on_remove = on_remove_function)]` /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::component::ComponentId; @@ -404,7 +405,8 @@ use thiserror::Error; /// This also supports function calls that yield closures /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # /// #[derive(Component)] @@ -481,7 +483,7 @@ use thiserror::Error; /// ``` /// # use std::cell::RefCell; /// # use bevy_ecs::component::Component; -/// use bevy_utils::synccell::SyncCell; +/// use bevy_platform::cell::SyncCell; /// /// // This will compile. /// #[derive(Component)] @@ -490,7 +492,7 @@ use thiserror::Error; /// } /// ``` /// -/// [`SyncCell`]: bevy_utils::synccell::SyncCell +/// [`SyncCell`]: bevy_platform::cell::SyncCell /// [`Exclusive`]: https://doc.rust-lang.org/nightly/std/sync/struct.Exclusive.html #[diagnostic::on_unimplemented( message = "`{Self}` is not a `Component`", @@ -595,7 +597,7 @@ mod private { /// `&mut ...`, created while inserted onto an entity. /// In all other ways, they are identical to mutable components. /// This restriction allows hooks to observe all changes made to an immutable -/// component, effectively turning the `OnInsert` and `OnReplace` hooks into a +/// component, effectively turning the `Insert` and `Replace` hooks into a /// `OnMutate` hook. /// This is not practical for mutable components, as the runtime cost of invoking /// a hook for every exclusive reference created would be far too high. @@ -621,6 +623,7 @@ pub trait ComponentMutability: private::Seal + 'static { pub struct Immutable; impl private::Seal for Immutable {} + impl ComponentMutability for Immutable { const MUTABLE: bool = false; } @@ -631,6 +634,7 @@ impl ComponentMutability for Immutable { pub struct Mutable; impl private::Seal for Mutable {} + impl ComponentMutability for Mutable { const MUTABLE: bool = true; } @@ -656,244 +660,6 @@ pub enum StorageType { SparseSet, } -/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. -pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); - -/// Context provided to a [`ComponentHook`]. -#[derive(Clone, Copy, Debug)] -pub struct HookContext { - /// The [`Entity`] this hook was invoked for. - pub entity: Entity, - /// The [`ComponentId`] this hook was invoked for. - pub component_id: ComponentId, - /// The caller location is `Some` if the `track_caller` feature is enabled. - pub caller: MaybeLocation, - /// Configures how relationship hooks will run - pub relationship_hook_mode: RelationshipHookMode, -} - -/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. -/// -/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. -/// These are intended to be used for structural side effects that need to happen when a component is added or removed, -/// and are not intended for general-purpose logic. -/// -/// For example, you might use a hook to update a cached index when a component is added, -/// to clean up resources when a component is removed, -/// or to keep hierarchical data structures across entities in sync. -/// -/// This information is stored in the [`ComponentInfo`] of the associated component. -/// -/// There is two ways of configuring hooks for a component: -/// 1. Defining the relevant hooks on the [`Component`] implementation -/// 2. Using the [`World::register_component_hooks`] method -/// -/// # Example 2 -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// use bevy_platform::collections::HashSet; -/// -/// #[derive(Component)] -/// struct MyTrackedComponent; -/// -/// #[derive(Resource, Default)] -/// struct TrackedEntities(HashSet); -/// -/// let mut world = World::new(); -/// world.init_resource::(); -/// -/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks -/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); -/// assert!(tracked_component_query.iter(&world).next().is_none()); -/// -/// world.register_component_hooks::().on_add(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.insert(context.entity); -/// }); -/// -/// world.register_component_hooks::().on_remove(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.remove(&context.entity); -/// }); -/// -/// let entity = world.spawn(MyTrackedComponent).id(); -/// let tracked_entities = world.resource::(); -/// assert!(tracked_entities.0.contains(&entity)); -/// -/// world.despawn(entity); -/// let tracked_entities = world.resource::(); -/// assert!(!tracked_entities.0.contains(&entity)); -/// ``` -#[derive(Debug, Clone, Default)] -pub struct ComponentHooks { - pub(crate) on_add: Option, - pub(crate) on_insert: Option, - pub(crate) on_replace: Option, - pub(crate) on_remove: Option, - pub(crate) on_despawn: Option, -} - -impl ComponentHooks { - pub(crate) fn update_from_component(&mut self) -> &mut Self { - if let Some(hook) = C::on_add() { - self.on_add(hook); - } - if let Some(hook) = C::on_insert() { - self.on_insert(hook); - } - if let Some(hook) = C::on_replace() { - self.on_replace(hook); - } - if let Some(hook) = C::on_remove() { - self.on_remove(hook); - } - if let Some(hook) = C::on_despawn() { - self.on_despawn(hook); - } - - self - } - - /// Register a [`ComponentHook`] that will be run when this component is added to an entity. - /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as - /// adding all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_add` hook - pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_add(hook) - .expect("Component already has an on_add hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// or replaced. - /// - /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_insert` hook - pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_insert(hook) - .expect("Component already has an on_insert hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, - /// such as being replaced (with `.insert`) or removed. - /// - /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, - /// allowing access to the previous data just before it is dropped. - /// This hook does *not* run if the entity did not already have this component. - /// - /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_replace` hook - pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_replace(hook) - .expect("Component already has an on_replace hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// Despawning an entity counts as removing all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_remove` hook - pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_remove(hook) - .expect("Component already has an on_remove hook") - } - - /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_despawn` hook - pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_despawn(hook) - .expect("Component already has an on_despawn hook") - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. - /// - /// This is a fallible version of [`Self::on_add`]. - /// - /// Returns `None` if the component already has an `on_add` hook. - pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_add.is_some() { - return None; - } - self.on_add = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// - /// This is a fallible version of [`Self::on_insert`]. - /// - /// Returns `None` if the component already has an `on_insert` hook. - pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_insert.is_some() { - return None; - } - self.on_insert = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed - /// - /// This is a fallible version of [`Self::on_replace`]. - /// - /// Returns `None` if the component already has an `on_replace` hook. - pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_replace.is_some() { - return None; - } - self.on_replace = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// - /// This is a fallible version of [`Self::on_remove`]. - /// - /// Returns `None` if the component already has an `on_remove` hook. - pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_remove.is_some() { - return None; - } - self.on_remove = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// This is a fallible version of [`Self::on_despawn`]. - /// - /// Returns `None` if the component already has an `on_despawn` hook. - pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_despawn.is_some() { - return None; - } - self.on_despawn = Some(hook); - Some(self) - } -} - /// Stores metadata for a type of component or resource stored in a specific [`World`]. #[derive(Debug, Clone)] pub struct ComponentInfo { @@ -913,8 +679,8 @@ impl ComponentInfo { /// Returns the name of the current component. #[inline] - pub fn name(&self) -> &str { - &self.descriptor.name + pub fn name(&self) -> DebugName { + self.descriptor.name.clone() } /// Returns `true` if the current component is mutable. @@ -1071,7 +837,7 @@ impl SparseSetIndex for ComponentId { /// A value describing a component or resource, which may or may not correspond to a Rust type. #[derive(Clone)] pub struct ComponentDescriptor { - name: Cow<'static, str>, + name: DebugName, // SAFETY: This must remain private. It must match the statically known StorageType of the // associated rust component type if one exists. storage_type: StorageType, @@ -1117,7 +883,7 @@ impl ComponentDescriptor { /// Create a new `ComponentDescriptor` for the type `T`. pub fn new() -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), storage_type: T::STORAGE_TYPE, is_send_and_sync: true, type_id: Some(TypeId::of::()), @@ -1142,7 +908,7 @@ impl ComponentDescriptor { clone_behavior: ComponentCloneBehavior, ) -> Self { Self { - name: name.into(), + name: name.into().into(), storage_type, is_send_and_sync: true, type_id: None, @@ -1158,7 +924,7 @@ impl ComponentDescriptor { /// The [`StorageType`] for resources is always [`StorageType::Table`]. pub fn new_resource() -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), // PERF: `SparseStorage` may actually be a more // reasonable choice as `storage_type` for resources. storage_type: StorageType::Table, @@ -1173,7 +939,7 @@ impl ComponentDescriptor { fn new_non_send(storage_type: StorageType) -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), storage_type, is_send_and_sync: false, type_id: Some(TypeId::of::()), @@ -1199,8 +965,8 @@ impl ComponentDescriptor { /// Returns the name of the current component. #[inline] - pub fn name(&self) -> &str { - self.name.as_ref() + pub fn name(&self) -> DebugName { + self.name.clone() } /// Returns whether this component is mutable. @@ -2052,7 +1818,7 @@ impl Components { } /// Gets the metadata associated with the given component, if it is registered. - /// This will return `None` if the id is not regiserted or is queued. + /// This will return `None` if the id is not registered or is queued. /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] @@ -2089,13 +1855,10 @@ impl Components { /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] - pub fn get_name<'a>(&'a self, id: ComponentId) -> Option> { + pub fn get_name<'a>(&'a self, id: ComponentId) -> Option { self.components .get(id.0) - .and_then(|info| { - info.as_ref() - .map(|info| Cow::Borrowed(info.descriptor.name())) - }) + .and_then(|info| info.as_ref().map(|info| info.descriptor.name())) .or_else(|| { let queued = self.queued.read().unwrap_or_else(PoisonError::into_inner); // first check components, then resources, then dynamic @@ -2400,7 +2163,7 @@ impl Components { /// * [`World::component_id()`] #[inline] pub fn valid_component_id(&self) -> Option { - self.get_id(TypeId::of::()) + self.get_valid_id(TypeId::of::()) } /// Type-erased equivalent of [`Components::valid_resource_id()`]. @@ -2431,7 +2194,7 @@ impl Components { /// * [`Components::get_resource_id()`] #[inline] pub fn valid_resource_id(&self) -> Option { - self.get_resource_id(TypeId::of::()) + self.get_valid_resource_id(TypeId::of::()) } /// Type-erased equivalent of [`Components::component_id()`]. @@ -2616,12 +2379,12 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub(crate) fn check_tick(&mut self, tick: Tick) -> bool { - let age = tick.relative_to(*self); + pub fn check_tick(&mut self, check: CheckChangeTicks) -> bool { + let age = check.present_tick().relative_to(*self); // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true // so long as this check always runs before that can happen. if age.get() > Self::MAX.get() { - *self = tick.relative_to(Self::MAX); + *self = check.present_tick().relative_to(Self::MAX); true } else { false @@ -2629,6 +2392,41 @@ impl Tick { } } +/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make +/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus +/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old. +/// +/// # Example +/// +/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change +/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on +/// long-running apps. +/// +/// To fix that, add an observer for this event that calls the schedule's +/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks). +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_ecs::component::CheckChangeTicks; +/// +/// #[derive(Resource)] +/// struct CustomSchedule(Schedule); +/// +/// # let mut world = World::new(); +/// world.add_observer(|check: On, mut schedule: ResMut| { +/// schedule.0.check_change_ticks(*check); +/// }); +/// ``` +#[derive(Debug, Clone, Copy, Event)] +pub struct CheckChangeTicks(pub(crate) Tick); + +impl CheckChangeTicks { + /// Get the present `Tick` that other ticks get compared to. + pub fn present_tick(self) -> Tick { + self.0 + } +} + /// Interior-mutable access to the [`Tick`]s for a single component or resource. #[derive(Copy, Clone, Debug)] pub struct TickCells<'a> { @@ -3013,13 +2811,13 @@ pub fn enforce_no_required_components_recursion( "Recursive required components detected: {}\nhelp: {}", recursion_check_stack .iter() - .map(|id| format!("{}", ShortName(&components.get_name(*id).unwrap()))) + .map(|id| format!("{}", components.get_name(*id).unwrap().shortname())) .collect::>() .join(" → "), if direct_recursion { format!( "Remove require({}).", - ShortName(&components.get_name(requiree).unwrap()) + components.get_name(requiree).unwrap().shortname() ) } else { "If this is intentional, consider merging the components.".into() @@ -3172,6 +2970,7 @@ impl Default for DefaultCloneBehaviorSpecialization { pub trait DefaultCloneBehaviorBase { fn default_clone_behavior(&self) -> ComponentCloneBehavior; } + impl DefaultCloneBehaviorBase for DefaultCloneBehaviorSpecialization { fn default_clone_behavior(&self) -> ComponentCloneBehavior { ComponentCloneBehavior::Default @@ -3183,6 +2982,7 @@ impl DefaultCloneBehaviorBase for DefaultCloneBehaviorSpecialization { pub trait DefaultCloneBehaviorViaClone { fn default_clone_behavior(&self) -> ComponentCloneBehavior; } + impl DefaultCloneBehaviorViaClone for &DefaultCloneBehaviorSpecialization { fn default_clone_behavior(&self) -> ComponentCloneBehavior { ComponentCloneBehavior::clone::() diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 5328eb1d3a..02d2491b7a 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1,10 +1,12 @@ use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_ptr::{Ptr, PtrMut}; +use bevy_utils::prelude::DebugName; use bumpalo::Bump; use core::any::TypeId; use crate::{ + archetype::Archetype, bundle::Bundle, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, @@ -170,7 +172,8 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// - `ComponentId` of component being written does not match expected `ComponentId`. pub fn write_target_component(&mut self, mut component: C) { C::map_entities(&mut component, &mut self.mapper); - let short_name = disqualified::ShortName::of::(); + let debug_name = DebugName::type_name::(); + let short_name = debug_name.shortname(); if self.target_component_written { panic!("Trying to write component '{short_name}' multiple times") } @@ -340,6 +343,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { pub struct EntityCloner { filter_allows_components: bool, filter: HashSet, + filter_required: HashSet, clone_behavior_overrides: HashMap, move_components: bool, linked_cloning: bool, @@ -356,6 +360,7 @@ impl Default for EntityCloner { linked_cloning: false, default_clone_fn: ComponentCloneBehavior::global_default_fn(), filter: Default::default(), + filter_required: Default::default(), clone_behavior_overrides: Default::default(), clone_queue: Default::default(), deferred_commands: Default::default(), @@ -459,6 +464,12 @@ impl EntityCloner { { let world = world.as_unsafe_world_cell(); let source_entity = world.get_entity(source).expect("Source entity must exist"); + let target_archetype = (!self.filter_required.is_empty()).then(|| { + world + .get_entity(target) + .expect("Target entity must exist") + .archetype() + }); #[cfg(feature = "bevy_reflect")] // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone @@ -475,7 +486,7 @@ impl EntityCloner { bundle_scratch = BundleScratch::with_capacity(archetype.component_count()); for component in archetype.components() { - if !self.is_cloning_allowed(&component) { + if !self.is_cloning_allowed(&component, target_archetype) { continue; } @@ -599,9 +610,19 @@ impl EntityCloner { target } - fn is_cloning_allowed(&self, component: &ComponentId) -> bool { - (self.filter_allows_components && self.filter.contains(component)) - || (!self.filter_allows_components && !self.filter.contains(component)) + fn is_cloning_allowed( + &self, + component: &ComponentId, + target_archetype: Option<&Archetype>, + ) -> bool { + if self.filter_allows_components { + self.filter.contains(component) + || target_archetype.is_some_and(|archetype| { + !archetype.contains(*component) && self.filter_required.contains(component) + }) + } else { + !self.filter.contains(component) && !self.filter_required.contains(component) + } } } @@ -686,7 +707,7 @@ impl<'w> EntityClonerBuilder<'w> { /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { for type_id in ids { - if let Some(id) = self.world.components().get_id(type_id) { + if let Some(id) = self.world.components().get_valid_id(type_id) { self.filter_allow(id); } } @@ -721,7 +742,7 @@ impl<'w> EntityClonerBuilder<'w> { /// Extends the list of components that shouldn't be cloned by type ids. pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { for type_id in ids { - if let Some(id) = self.world.components().get_id(type_id) { + if let Some(id) = self.world.components().get_valid_id(type_id) { self.filter_deny(id); } } @@ -743,7 +764,7 @@ impl<'w> EntityClonerBuilder<'w> { &mut self, clone_behavior: ComponentCloneBehavior, ) -> &mut Self { - if let Some(id) = self.world.components().component_id::() { + if let Some(id) = self.world.components().valid_component_id::() { self.entity_cloner .clone_behavior_overrides .insert(id, clone_behavior); @@ -768,7 +789,7 @@ impl<'w> EntityClonerBuilder<'w> { /// Removes a previously set override of [`ComponentCloneBehavior`] for a component in this builder. pub fn remove_clone_behavior_override(&mut self) -> &mut Self { - if let Some(id) = self.world.components().component_id::() { + if let Some(id) = self.world.components().valid_component_id::() { self.entity_cloner.clone_behavior_overrides.remove(&id); } self @@ -803,9 +824,9 @@ impl<'w> EntityClonerBuilder<'w> { if let Some(info) = self.world.components().get_info(id) { for required_id in info.required_components().iter_ids() { if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.insert(required_id); + self.entity_cloner.filter_required.insert(required_id); } else { - self.entity_cloner.filter.remove(&required_id); + self.entity_cloner.filter_required.remove(&required_id); } } } @@ -823,9 +844,9 @@ impl<'w> EntityClonerBuilder<'w> { if let Some(info) = self.world.components().get_info(id) { for required_id in info.required_components().iter_ids() { if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.remove(&required_id); + self.entity_cloner.filter_required.remove(&required_id); } else { - self.entity_cloner.filter.insert(required_id); + self.entity_cloner.filter_required.insert(required_id); } } } @@ -1400,4 +1421,36 @@ mod tests { ); assert!(world.resource::().0); } + + #[test] + fn cloning_with_required_components_preserves_existing() { + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u32); + + let mut world = World::default(); + + let e = world.spawn((A, B(0))).id(); + let e_clone = world.spawn(B(1)).id(); + + EntityCloner::build(&mut world) + .deny_all() + .allow::() + .clone_entity(e, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&A)); + assert_eq!(world.entity(e_clone).get::(), Some(&B(1))); + + let e_clone2 = world.spawn(B(2)).id(); + + EntityCloner::build(&mut world) + .allow_all() + .deny::() + .clone_entity(e, e_clone2); + + assert_eq!(world.entity(e_clone2).get::(), Some(&B(2))); + } } diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index 3dac2fa749..647bde983a 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -358,7 +358,10 @@ mod tests { // Next allocated entity should be a further generation on the same index let entity = world.spawn_empty().id(); assert_eq!(entity.index(), dead_ref.index()); - assert!(entity.generation() > dead_ref.generation()); + assert!(entity + .generation() + .cmp_approx(&dead_ref.generation()) + .is_gt()); } #[test] @@ -373,7 +376,10 @@ mod tests { // Next allocated entity should be a further generation on the same index let entity = world.spawn_empty().id(); assert_eq!(entity.index(), dead_ref.index()); - assert!(entity.generation() > dead_ref.generation()); + assert!(entity + .generation() + .cmp_approx(&dead_ref.generation()) + .is_gt()); } #[test] diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index e4d4b26d97..700a4e517f 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -76,18 +76,12 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, - component::Tick, + component::{CheckChangeTicks, Tick}, storage::{SparseSetIndex, TableId, TableRow}, }; use alloc::vec::Vec; use bevy_platform::sync::atomic::Ordering; -use core::{ - fmt, - hash::Hash, - mem::{self, MaybeUninit}, - num::NonZero, - panic::Location, -}; +use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; use log::warn; #[cfg(feature = "serialize")] @@ -189,8 +183,25 @@ impl SparseSetIndex for EntityRow { /// This tracks different versions or generations of an [`EntityRow`]. /// Importantly, this can wrap, meaning each generation is not necessarily unique per [`EntityRow`]. /// -/// This should be treated as a opaque identifier, and it's internal representation may be subject to change. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display)] +/// This should be treated as a opaque identifier, and its internal representation may be subject to change. +/// +/// # Aliasing +/// +/// Internally [`EntityGeneration`] wraps a `u32`, so it can't represent *every* possible generation. +/// Eventually, generations can (and do) wrap or alias. +/// This can cause [`Entity`] and [`EntityGeneration`] values to be equal while still referring to different conceptual entities. +/// This can cause some surprising behavior: +/// +/// ``` +/// # use bevy_ecs::entity::EntityGeneration; +/// let (aliased, did_alias) = EntityGeneration::FIRST.after_versions(1u32 << 31).after_versions_and_could_alias(1u32 << 31); +/// assert!(did_alias); +/// assert!(EntityGeneration::FIRST == aliased); +/// ``` +/// +/// This can cause some unintended side effects. +/// See [`Entity`] docs for practical concerns and how to minimize any risks. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Display)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(opaque))] #[cfg_attr(feature = "bevy_reflect", reflect(Hash, PartialEq, Debug, Clone))] @@ -201,6 +212,9 @@ impl EntityGeneration { /// Represents the first generation of an [`EntityRow`]. pub const FIRST: Self = Self(0); + /// Non-wrapping difference between two generations after which a signed interpretation becomes negative. + const DIFF_MAX: u32 = 1u32 << 31; + /// Gets some bits that represent this value. /// The bits are opaque and should not be regarded as meaningful. #[inline(always)] @@ -232,11 +246,54 @@ impl EntityGeneration { let raw = self.0.overflowing_add(versions); (Self(raw.0), raw.1) } + + /// Compares two generations. + /// + /// Generations that are later will be [`Greater`](core::cmp::Ordering::Greater) than earlier ones. + /// + /// ``` + /// # use bevy_ecs::entity::EntityGeneration; + /// # use core::cmp::Ordering; + /// let later_generation = EntityGeneration::FIRST.after_versions(400); + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&later_generation), Ordering::Less); + /// + /// let (aliased, did_alias) = EntityGeneration::FIRST.after_versions(400).after_versions_and_could_alias(u32::MAX); + /// assert!(did_alias); + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&aliased), Ordering::Less); + /// ``` + /// + /// Ordering will be incorrect and [non-transitive](https://en.wikipedia.org/wiki/Transitive_relation) + /// for distant generations: + /// + /// ```should_panic + /// # use bevy_ecs::entity::EntityGeneration; + /// # use core::cmp::Ordering; + /// let later_generation = EntityGeneration::FIRST.after_versions(3u32 << 31); + /// let much_later_generation = later_generation.after_versions(3u32 << 31); + /// + /// // while these orderings are correct and pass assertions... + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&later_generation), Ordering::Less); + /// assert_eq!(later_generation.cmp_approx(&much_later_generation), Ordering::Less); + /// + /// // ... this ordering is not and the assertion fails! + /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&much_later_generation), Ordering::Less); + /// ``` + /// + /// Because of this, `EntityGeneration` does not implement `Ord`/`PartialOrd`. + #[inline] + pub const fn cmp_approx(&self, other: &Self) -> core::cmp::Ordering { + use core::cmp::Ordering; + match self.0.wrapping_sub(other.0) { + 0 => Ordering::Equal, + 1..Self::DIFF_MAX => Ordering::Greater, + _ => Ordering::Less, + } + } } /// Lightweight identifier of an [entity](crate::entity). /// -/// The identifier is implemented using a [generational index]: a combination of an index and a generation. +/// The identifier is implemented using a [generational index]: a combination of an index ([`EntityRow`]) and a generation ([`EntityGeneration`]). /// This allows fast insertion after data removal in an array while minimizing loss of spatial locality. /// /// These identifiers are only valid on the [`World`] it's sourced from. Attempting to use an `Entity` to @@ -244,6 +301,19 @@ impl EntityGeneration { /// /// [generational index]: https://lucassardois.medium.com/generational-indices-guide-8e3c5f7fd594 /// +/// # Aliasing +/// +/// Once an entity is despawned, it ceases to exist. +/// However, its [`Entity`] id is still present, and may still be contained in some data. +/// This becomes problematic because it is possible for a later entity to be spawned at the exact same id! +/// If this happens, which is rare but very possible, it will be logged. +/// +/// Aliasing can happen without warning. +/// Holding onto a [`Entity`] id corresponding to an entity well after that entity was despawned can cause un-intuitive behavior for both ordering, and comparing in general. +/// To prevent these bugs, it is generally best practice to stop holding an [`Entity`] or [`EntityGeneration`] value as soon as you know it has been despawned. +/// If you must do otherwise, do not assume the [`Entity`] corresponds to the same conceptual entity it originally did. +/// See [`EntityGeneration`]'s docs for more information about aliasing and why it occurs. +/// /// # Stability warning /// For all intents and purposes, `Entity` should be treated as an opaque identifier. The internal bit /// representation is liable to change from release to release as are the behaviors or performance @@ -648,6 +718,7 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { } impl<'a> ExactSizeIterator for ReserveEntitiesIterator<'a> {} + impl<'a> core::iter::FusedIterator for ReserveEntitiesIterator<'a> {} // SAFETY: Newly reserved entity values are unique. @@ -826,8 +897,10 @@ impl Entities { /// Destroy an entity, allowing it to be reused. /// + /// Returns the `Option` of the entity or `None` if the `entity` was not present. + /// /// Must not be called while reserved entities are awaiting `flush()`. - pub fn free(&mut self, entity: Entity) -> Option { + pub fn free(&mut self, entity: Entity) -> Option { self.verify_flushed(); let meta = &mut self.meta[entity.index() as usize]; @@ -889,58 +962,46 @@ impl Entities { *self.free_cursor.get_mut() = 0; } - /// Returns the location of an [`Entity`]. - /// Note: for pending entities, returns `None`. + /// Returns the [`EntityLocation`] of an [`Entity`]. + /// Note: for pending entities and entities not participating in the ECS (entities with a [`EntityIdLocation`] of `None`), returns `None`. #[inline] pub fn get(&self, entity: Entity) -> Option { - if let Some(meta) = self.meta.get(entity.index() as usize) { - if meta.generation != entity.generation - || meta.location.archetype_id == ArchetypeId::INVALID - { - return None; - } - Some(meta.location) - } else { - None - } + self.get_id_location(entity).flatten() } - /// Updates the location of an [`Entity`]. This must be called when moving the components of - /// the existing entity around in storage. - /// - /// For spawning and despawning entities, [`set_spawn_despawn`](Self::set_spawn_despawn) must - /// be used instead. + /// Returns the [`EntityIdLocation`] of an [`Entity`]. + /// Note: for pending entities, returns `None`. + #[inline] + pub fn get_id_location(&self, entity: Entity) -> Option { + self.meta + .get(entity.index() as usize) + .filter(|meta| meta.generation == entity.generation) + .map(|meta| meta.location) + } + + /// Updates the location of an [`Entity`]. + /// This must be called when moving the components of the existing entity around in storage. /// /// # Safety /// - `index` must be a valid entity index. /// - `location` must be valid for the entity at `index` or immediately made valid afterwards /// before handing control to unknown code. #[inline] - pub(crate) unsafe fn set(&mut self, index: u32, location: EntityLocation) { + pub(crate) unsafe fn set(&mut self, index: u32, location: EntityIdLocation) { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; meta.location = location; } - /// Updates the location of an [`Entity`]. This must be called when moving the components of - /// the spawned or despawned entity around in storage. + /// Mark an [`Entity`] as spawned or despawned in the given tick. /// /// # Safety /// - `index` must be a valid entity index. - /// - `location` must be valid for the entity at `index` or immediately made valid afterwards - /// before handing control to unknown code. #[inline] - pub(crate) unsafe fn set_spawn_despawn( - &mut self, - index: u32, - location: EntityLocation, - by: MaybeLocation, - at: Tick, - ) { + pub(crate) unsafe fn mark_spawn_despawn(&mut self, index: u32, by: MaybeLocation, at: Tick) { // SAFETY: Caller guarantees that `index` a valid entity index let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; - meta.location = location; - meta.spawned_or_despawned = MaybeUninit::new(SpawnedOrDespawned { by, at }); + meta.spawned_or_despawned = SpawnedOrDespawned { by, at }; } /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this @@ -954,7 +1015,7 @@ impl Entities { } let meta = &mut self.meta[index as usize]; - if meta.location.archetype_id == ArchetypeId::INVALID { + if meta.location.is_none() { meta.generation = meta.generation.after_versions(generations); true } else { @@ -989,6 +1050,8 @@ impl Entities { /// Allocates space for entities previously reserved with [`reserve_entity`](Entities::reserve_entity) or /// [`reserve_entities`](Entities::reserve_entities), then initializes each one using the supplied function. /// + /// See [`EntityLocation`] for details on its meaning and how to set it. + /// /// # Safety /// Flush _must_ set the entity location to the correct [`ArchetypeId`] for the given [`Entity`] /// each time init is called. This _can_ be [`ArchetypeId::INVALID`], provided the [`Entity`] @@ -996,7 +1059,12 @@ impl Entities { /// /// Note: freshly-allocated entities (ones which don't come from the pending list) are guaranteed /// to be initialized with the invalid archetype. - pub unsafe fn flush(&mut self, mut init: impl FnMut(Entity, &mut EntityLocation)) { + pub unsafe fn flush( + &mut self, + mut init: impl FnMut(Entity, &mut EntityIdLocation), + by: MaybeLocation, + at: Tick, + ) { let free_cursor = self.free_cursor.get_mut(); let current_free_cursor = *free_cursor; @@ -1013,6 +1081,7 @@ impl Entities { Entity::from_raw_and_generation(row, meta.generation), &mut meta.location, ); + meta.spawned_or_despawned = SpawnedOrDespawned { by, at }; } *free_cursor = 0; @@ -1025,18 +1094,23 @@ impl Entities { Entity::from_raw_and_generation(row, meta.generation), &mut meta.location, ); + meta.spawned_or_despawned = SpawnedOrDespawned { by, at }; } } /// Flushes all reserved entities to an "invalid" state. Attempting to retrieve them will return `None` /// unless they are later populated with a valid archetype. - pub fn flush_as_invalid(&mut self) { + pub fn flush_as_invalid(&mut self, by: MaybeLocation, at: Tick) { // SAFETY: as per `flush` safety docs, the archetype id can be set to [`ArchetypeId::INVALID`] if // the [`Entity`] has not been assigned to an [`Archetype`][crate::archetype::Archetype], which is the case here unsafe { - self.flush(|_entity, location| { - location.archetype_id = ArchetypeId::INVALID; - }); + self.flush( + |_entity, location| { + *location = None; + }, + by, + at, + ); } } @@ -1083,8 +1157,10 @@ impl Entities { self.len() == 0 } - /// Returns the source code location from which this entity has last been spawned - /// or despawned. Returns `None` if its index has been reused by another entity + /// Try to get the source code location from which this entity has last been + /// spawned, despawned or flushed. + /// + /// Returns `None` if its index has been reused by another entity /// or if this entity has never existed. pub fn entity_get_spawned_or_despawned_by( &self, @@ -1096,17 +1172,21 @@ impl Entities { }) } - /// Returns the [`Tick`] at which this entity has last been spawned or despawned. + /// Try to get the [`Tick`] at which this entity has last been + /// spawned, despawned or flushed. + /// /// Returns `None` if its index has been reused by another entity or if this entity - /// has never existed. + /// has never been spawned. pub fn entity_get_spawned_or_despawned_at(&self, entity: Entity) -> Option { self.entity_get_spawned_or_despawned(entity) .map(|spawned_or_despawned| spawned_or_despawned.at) } - /// Returns the [`SpawnedOrDespawned`] related to the entity's last spawn or - /// respawn. Returns `None` if its index has been reused by another entity or if - /// this entity has never existed. + /// Try to get the [`SpawnedOrDespawned`] related to the entity's last spawn, + /// despawn or flush. + /// + /// Returns `None` if its index has been reused by another entity or if + /// this entity has never been spawned. #[inline] fn entity_get_spawned_or_despawned(&self, entity: Entity) -> Option { self.meta @@ -1114,12 +1194,9 @@ impl Entities { .filter(|meta| // Generation is incremented immediately upon despawn (meta.generation == entity.generation) - || (meta.location.archetype_id == ArchetypeId::INVALID) + || meta.location.is_none() && (meta.generation == entity.generation.after_versions(1))) - .map(|meta| { - // SAFETY: valid archetype or non-min generation is proof this is init - unsafe { meta.spawned_or_despawned.assume_init() } - }) + .map(|meta| meta.spawned_or_despawned) } /// Returns the source code location from which this entity has last been spawned @@ -1136,21 +1213,13 @@ impl Entities { ) -> (MaybeLocation, Tick) { // SAFETY: caller ensures entity is allocated let meta = unsafe { self.meta.get_unchecked(entity.index() as usize) }; - // SAFETY: caller ensures entities of this index were at least spawned - let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init() }; - (spawned_or_despawned.by, spawned_or_despawned.at) + (meta.spawned_or_despawned.by, meta.spawned_or_despawned.at) } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for meta in &mut self.meta { - if meta.generation != EntityGeneration::FIRST - || meta.location.archetype_id != ArchetypeId::INVALID - { - // SAFETY: non-min generation or valid archetype is proof this is init - let spawned_or_despawned = unsafe { meta.spawned_or_despawned.assume_init_mut() }; - spawned_or_despawned.at.check_tick(change_tick); - } + meta.spawned_or_despawned.at.check_tick(check); } } @@ -1210,11 +1279,11 @@ impl fmt::Display for EntityDoesNotExistDetails { #[derive(Copy, Clone, Debug)] struct EntityMeta { /// The current [`EntityGeneration`] of the [`EntityRow`]. - pub generation: EntityGeneration, - /// The current location of the [`EntityRow`] - pub location: EntityLocation, - /// Location of the last spawn or despawn of this entity - spawned_or_despawned: MaybeUninit, + generation: EntityGeneration, + /// The current location of the [`EntityRow`]. + location: EntityIdLocation, + /// Location and tick of the last spawn, despawn or flush of this entity. + spawned_or_despawned: SpawnedOrDespawned, } #[derive(Copy, Clone, Debug)] @@ -1227,8 +1296,11 @@ impl EntityMeta { /// meta for **pending entity** const EMPTY: EntityMeta = EntityMeta { generation: EntityGeneration::FIRST, - location: EntityLocation::INVALID, - spawned_or_despawned: MaybeUninit::uninit(), + location: None, + spawned_or_despawned: SpawnedOrDespawned { + by: MaybeLocation::caller(), + at: Tick::new(0), + }, }; } @@ -1256,15 +1328,15 @@ pub struct EntityLocation { pub table_row: TableRow, } -impl EntityLocation { - /// location for **pending entity** and **invalid entity** - pub(crate) const INVALID: EntityLocation = EntityLocation { - archetype_id: ArchetypeId::INVALID, - archetype_row: ArchetypeRow::INVALID, - table_id: TableId::INVALID, - table_row: TableRow::INVALID, - }; -} +/// An [`Entity`] id may or may not correspond to a valid conceptual entity. +/// If it does, the conceptual entity may or may not have a location. +/// If it has no location, the [`EntityLocation`] will be `None`. +/// An location of `None` means the entity effectively does not exist; it has an id, but is not participating in the ECS. +/// This is different from a location in the empty archetype, which is participating (queryable, etc) but just happens to have no components. +/// +/// Setting a location to `None` is often helpful when you want to destruct an entity or yank it from the ECS without allowing another system to reuse the id for something else. +/// It is also useful for reserving an id; commands will often allocate an `Entity` but not provide it a location until the command is applied. +pub type EntityIdLocation = Option; #[cfg(test)] mod tests { @@ -1294,7 +1366,7 @@ mod tests { let mut e = Entities::new(); e.reserve_entity(); // SAFETY: entity_location is left invalid - unsafe { e.flush(|_, _| {}) }; + unsafe { e.flush(|_, _| {}, MaybeLocation::caller(), Tick::default()) }; assert_eq!(e.len(), 1); } @@ -1307,9 +1379,13 @@ mod tests { // SAFETY: entity_location is left invalid unsafe { - entities.flush(|_entity, _location| { - // do nothing ... leaving entity location invalid - }); + entities.flush( + |_entity, _location| { + // do nothing ... leaving entity location invalid + }, + MaybeLocation::caller(), + Tick::default(), + ); }; assert!(entities.contains(e)); @@ -1357,7 +1433,10 @@ mod tests { // The very next entity allocated should be a further generation on the same index let next_entity = entities.alloc(); assert_eq!(next_entity.index(), entity.index()); - assert!(next_entity.generation() > entity.generation().after_versions(GENERATIONS)); + assert!(next_entity + .generation() + .cmp_approx(&entity.generation().after_versions(GENERATIONS)) + .is_gt()); } #[test] @@ -1544,25 +1623,43 @@ mod tests { } } + #[test] + fn entity_generation_is_approximately_ordered() { + use core::cmp::Ordering; + + let old = EntityGeneration::FIRST; + let middle = old.after_versions(1); + let younger_before_ord_wrap = middle.after_versions(EntityGeneration::DIFF_MAX); + let younger_after_ord_wrap = younger_before_ord_wrap.after_versions(1); + + assert_eq!(middle.cmp_approx(&old), Ordering::Greater); + assert_eq!(middle.cmp_approx(&middle), Ordering::Equal); + assert_eq!(middle.cmp_approx(&younger_before_ord_wrap), Ordering::Less); + assert_eq!( + middle.cmp_approx(&younger_after_ord_wrap), + Ordering::Greater + ); + } + #[test] fn entity_debug() { let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); - let string = format!("{:?}", entity); + let string = format!("{entity:?}"); assert_eq!(string, "42v0#4294967253"); let entity = Entity::PLACEHOLDER; - let string = format!("{:?}", entity); + let string = format!("{entity:?}"); assert_eq!(string, "PLACEHOLDER"); } #[test] fn entity_display() { let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap())); - let string = format!("{}", entity); + let string = format!("{entity}"); assert_eq!(string, "42v0"); let entity = Entity::PLACEHOLDER; - let string = format!("{}", entity); + let string = format!("{entity}"); assert_eq!(string, "PLACEHOLDER"); } } diff --git a/crates/bevy_ecs/src/entity/unique_array.rs b/crates/bevy_ecs/src/entity/unique_array.rs index ce31e55448..71df33ec5f 100644 --- a/crates/bevy_ecs/src/entity/unique_array.rs +++ b/crates/bevy_ecs/src/entity/unique_array.rs @@ -154,6 +154,7 @@ impl DerefMut for UniqueEntityEquivalentArr unsafe { UniqueEntityEquivalentSlice::from_slice_unchecked_mut(&mut self.0) } } } + impl Default for UniqueEntityEquivalentArray { fn default() -> Self { Self(Default::default()) @@ -527,6 +528,7 @@ impl, U: EntityEquivalent, const N: usize> self.eq(&other.0) } } + impl, U: EntityEquivalent, const N: usize> PartialEq<&UniqueEntityEquivalentArray> for VecDeque { @@ -550,6 +552,7 @@ impl, U: EntityEquivalent, const N: usize> self.eq(&other.0) } } + impl, U: EntityEquivalent, const N: usize> PartialEq> for VecDeque { diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index 0686e68f1d..c290e249b2 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -76,7 +76,7 @@ impl BevyError { break; } } - writeln!(f, "{}", line)?; + writeln!(f, "{line}")?; } if !full_backtrace { if std::thread::panicking() { diff --git a/crates/bevy_ecs/src/error/command_handling.rs b/crates/bevy_ecs/src/error/command_handling.rs index d85ad4a87e..c303b76d17 100644 --- a/crates/bevy_ecs/src/error/command_handling.rs +++ b/crates/bevy_ecs/src/error/command_handling.rs @@ -1,4 +1,6 @@ -use core::{any::type_name, fmt}; +use core::fmt; + +use bevy_utils::prelude::DebugName; use crate::{ entity::Entity, @@ -7,22 +9,17 @@ use crate::{ world::{error::EntityMutableFetchError, World}, }; -use super::{default_error_handler, BevyError, ErrorContext}; +use super::{BevyError, ErrorContext, ErrorHandler}; -/// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into +/// Takes a [`Command`] that potentially returns a Result and uses a given error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. -pub trait HandleError { +pub trait HandleError: Send + 'static { /// Takes a [`Command`] that returns a Result and uses a given error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. - fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command; + fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command; /// Takes a [`Command`] that returns a Result and uses the default error handler function to convert it into /// a [`Command`] that internally handles an error if it occurs and returns `()`. - fn handle_error(self) -> impl Command - where - Self: Sized, - { - self.handle_error_with(default_error_handler()) - } + fn handle_error(self) -> impl Command; } impl HandleError> for C @@ -30,13 +27,25 @@ where C: Command>, E: Into, { - fn handle_error_with(self, error_handler: fn(BevyError, ErrorContext)) -> impl Command { + fn handle_error_with(self, error_handler: ErrorHandler) -> impl Command { move |world: &mut World| match self.apply(world) { Ok(_) => {} Err(err) => (error_handler)( err.into(), ErrorContext::Command { - name: type_name::().into(), + name: DebugName::type_name::(), + }, + ), + } + } + + fn handle_error(self) -> impl Command { + move |world: &mut World| match self.apply(world) { + Ok(_) => {} + Err(err) => world.default_error_handler()( + err.into(), + ErrorContext::Command { + name: DebugName::type_name::(), }, ), } @@ -52,6 +61,13 @@ where self.apply(world); } } + + #[inline] + fn handle_error(self) -> impl Command { + move |world: &mut World| { + self.apply(world); + } + } } impl HandleError for C @@ -63,10 +79,7 @@ where self } #[inline] - fn handle_error(self) -> impl Command - where - Self: Sized, - { + fn handle_error(self) -> impl Command { self } } diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index 688b599473..85a5a13297 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -1,9 +1,8 @@ -#[cfg(feature = "configurable_error_handler")] -use bevy_platform::sync::OnceLock; use core::fmt::Display; -use crate::{component::Tick, error::BevyError}; -use alloc::borrow::Cow; +use crate::{component::Tick, error::BevyError, prelude::Resource}; +use bevy_utils::prelude::DebugName; +use derive_more::derive::{Deref, DerefMut}; /// Context for a [`BevyError`] to aid in debugging. #[derive(Debug, PartialEq, Eq, Clone)] @@ -11,26 +10,26 @@ pub enum ErrorContext { /// The error occurred in a system. System { /// The name of the system that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the system was run. last_run: Tick, }, /// The error occurred in a run condition. RunCondition { /// The name of the run condition that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the run condition was evaluated. last_run: Tick, }, /// The error occurred in a command. Command { /// The name of the command that failed. - name: Cow<'static, str>, + name: DebugName, }, /// The error occurred in an observer. Observer { /// The name of the observer that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the observer was run. last_run: Tick, }, @@ -40,14 +39,14 @@ impl Display for ErrorContext { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::System { name, .. } => { - write!(f, "System `{}` failed", name) + write!(f, "System `{name}` failed") } - Self::Command { name } => write!(f, "Command `{}` failed", name), + Self::Command { name } => write!(f, "Command `{name}` failed"), Self::Observer { name, .. } => { - write!(f, "Observer `{}` failed", name) + write!(f, "Observer `{name}` failed") } Self::RunCondition { name, .. } => { - write!(f, "Run condition `{}` failed", name) + write!(f, "Run condition `{name}` failed") } } } @@ -55,12 +54,12 @@ impl Display for ErrorContext { impl ErrorContext { /// The name of the ECS construct that failed. - pub fn name(&self) -> &str { + pub fn name(&self) -> DebugName { match self { Self::System { name, .. } | Self::Command { name, .. } | Self::Observer { name, .. } - | Self::RunCondition { name, .. } => name, + | Self::RunCondition { name, .. } => name.clone(), } } @@ -77,53 +76,6 @@ impl ErrorContext { } } -/// A global error handler. This can be set at startup, as long as it is set before -/// any uses. This should generally be configured _before_ initializing the app. -/// -/// This should be set inside of your `main` function, before initializing the Bevy app. -/// The value of this error handler can be accessed using the [`default_error_handler`] function, -/// which calls [`OnceLock::get_or_init`] to get the value. -/// -/// **Note:** this is only available when the `configurable_error_handler` feature of `bevy_ecs` (or `bevy`) is enabled! -/// -/// # Example -/// -/// ``` -/// # use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, warn}; -/// GLOBAL_ERROR_HANDLER.set(warn).expect("The error handler can only be set once, globally."); -/// // initialize Bevy App here -/// ``` -/// -/// To use this error handler in your app for custom error handling logic: -/// -/// ```rust -/// use bevy_ecs::error::{default_error_handler, GLOBAL_ERROR_HANDLER, BevyError, ErrorContext, panic}; -/// -/// fn handle_errors(error: BevyError, ctx: ErrorContext) { -/// let error_handler = default_error_handler(); -/// error_handler(error, ctx); -/// } -/// ``` -/// -/// # Warning -/// -/// As this can *never* be overwritten, library code should never set this value. -#[cfg(feature = "configurable_error_handler")] -pub static GLOBAL_ERROR_HANDLER: OnceLock = OnceLock::new(); - -/// The default error handler. This defaults to [`panic()`], -/// but if set, the [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization. -/// The `configurable_error_handler` feature must be enabled to change this from the panicking default behavior, -/// as there may be runtime overhead. -#[inline] -pub fn default_error_handler() -> fn(BevyError, ErrorContext) { - #[cfg(not(feature = "configurable_error_handler"))] - return panic; - - #[cfg(feature = "configurable_error_handler")] - return *GLOBAL_ERROR_HANDLER.get_or_init(|| panic); -} - macro_rules! inner { ($call:path, $e:ident, $c:ident) => { $call!( @@ -135,6 +87,25 @@ macro_rules! inner { }; } +/// Defines how Bevy reacts to errors. +pub type ErrorHandler = fn(BevyError, ErrorContext); + +/// Error handler to call when an error is not handled otherwise. +/// Defaults to [`panic()`]. +/// +/// When updated while a [`Schedule`] is running, it doesn't take effect for +/// that schedule until it's completed. +/// +/// [`Schedule`]: crate::schedule::Schedule +#[derive(Resource, Deref, DerefMut, Copy, Clone)] +pub struct DefaultErrorHandler(pub ErrorHandler); + +impl Default for DefaultErrorHandler { + fn default() -> Self { + Self(panic) + } +} + /// Error handler that panics with the system error. #[track_caller] #[inline] diff --git a/crates/bevy_ecs/src/error/mod.rs b/crates/bevy_ecs/src/error/mod.rs index 950deee3ec..231bdda940 100644 --- a/crates/bevy_ecs/src/error/mod.rs +++ b/crates/bevy_ecs/src/error/mod.rs @@ -7,8 +7,9 @@ //! All [`BevyError`]s returned by a system, observer or command are handled by an "error handler". By default, the //! [`panic`] error handler function is used, resulting in a panic with the error message attached. //! -//! You can change the default behavior by registering a custom error handler. -//! Modify the [`GLOBAL_ERROR_HANDLER`] value to set a custom error handler function for your entire app. +//! You can change the default behavior by registering a custom error handler: +//! Use [`DefaultErrorHandler`] to set a custom error handler function for a world, +//! or `App::set_error_handler` for a whole app. //! In practice, this is generally feature-flagged: panicking or loudly logging errors in development, //! and quietly logging or ignoring them in production to avoid crashing the app. //! @@ -33,10 +34,8 @@ //! The [`ErrorContext`] allows you to access additional details relevant to providing //! context surrounding the error – such as the system's [`name`] – in your error messages. //! -//! Remember to turn on the `configurable_error_handler` feature to set a global error handler! -//! //! ```rust, ignore -//! use bevy_ecs::error::{GLOBAL_ERROR_HANDLER, BevyError, ErrorContext}; +//! use bevy_ecs::error::{BevyError, ErrorContext, DefaultErrorHandler}; //! use log::trace; //! //! fn my_error_handler(error: BevyError, ctx: ErrorContext) { @@ -48,10 +47,9 @@ //! } //! //! fn main() { -//! // This requires the "configurable_error_handler" feature to be enabled to be in scope. -//! GLOBAL_ERROR_HANDLER.set(my_error_handler).expect("The error handler can only be set once."); -//! -//! // Initialize your Bevy App here +//! let mut world = World::new(); +//! world.insert_resource(DefaultErrorHandler(my_error_handler)); +//! // Use your world here //! } //! ``` //! diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index d525ba2e57..52839f369d 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,33 +11,81 @@ use core::{ marker::PhantomData, }; -/// Something that "happens" and might be read / observed by app logic. +/// Something that "happens" and can be processed by app logic. /// -/// Events can be stored in an [`Events`] resource -/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter. +/// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), +/// causing any global [`Observer`] watching that event to run. This allows for push-based +/// event handling where observers are immediately notified of events as they happen. /// -/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run. +/// Additional event handling behavior can be enabled by implementing the [`EntityEvent`] +/// and [`BufferedEvent`] traits: +/// +/// - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. +/// They are useful for entity-specific event handlers and can even be propagated from one entity to another. +/// - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] +/// and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing +/// of events at fixed points in a schedule. /// /// Events must be thread-safe. /// -/// ## Derive -/// This trait can be derived. -/// Adding `auto_propagate` sets [`Self::AUTO_PROPAGATE`] to true. -/// Adding `traversal = "X"` sets [`Self::Traversal`] to be of type "X". +/// # Usage +/// +/// The [`Event`] trait can be derived: /// /// ``` -/// use bevy_ecs::prelude::*; -/// +/// # use bevy_ecs::prelude::*; +/// # /// #[derive(Event)] -/// #[event(auto_propagate)] -/// struct MyEvent; +/// struct Speak { +/// message: String, +/// } /// ``` /// +/// An [`Observer`] can then be added to listen for this event type: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.message); +/// }); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// # world.add_observer(|trigger: On| { +/// # println!("{}", trigger.message); +/// # }); +/// # +/// # world.flush(); +/// # +/// world.trigger(Speak { +/// message: "Hello!".to_string(), +/// }); +/// ``` +/// +/// For events that additionally need entity targeting or buffering, consider also deriving +/// [`EntityEvent`] or [`BufferedEvent`], respectively. /// /// [`World`]: crate::world::World -/// [`ComponentId`]: crate::component::ComponentId /// [`Observer`]: crate::observer::Observer -/// [`Events`]: super::Events /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( @@ -46,18 +94,6 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(Event)]`" )] pub trait Event: Send + Sync + 'static { - /// The component that describes which Entity to propagate this event to next, when [propagation] is enabled. - /// - /// [propagation]: crate::observer::Trigger::propagate - type Traversal: Traversal; - - /// When true, this event will always attempt to propagate when [triggered], without requiring a call - /// to [`Trigger::propagate`]. - /// - /// [triggered]: crate::system::Commands::trigger_targets - /// [`Trigger::propagate`]: crate::observer::Trigger::propagate - const AUTO_PROPAGATE: bool = false; - /// Generates the [`ComponentId`] for this event type. /// /// If this type has already been registered, @@ -68,7 +104,7 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, + /// This method should not be overridden by implementers, /// and should always correspond to the implementation of [`component_id`](Event::component_id). fn register_component_id(world: &mut World) -> ComponentId { world.register_component::>() @@ -82,13 +118,216 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, + /// This method should not be overridden by implementers, /// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id). fn component_id(world: &World) -> Option { world.component_id::>() } } +/// An [`Event`] that can be targeted at specific entities. +/// +/// Entity events can be triggered on a [`World`] with specific entity targets using a method +/// like [`trigger_targets`](World::trigger_targets), causing any [`Observer`] watching the event +/// for those entities to run. +/// +/// Unlike basic [`Event`]s, entity events can optionally be propagated from one entity target to another +/// based on the [`EntityEvent::Traversal`] type associated with the event. This enables use cases +/// such as bubbling events to parent entities for UI purposes. +/// +/// Entity events must be thread-safe. +/// +/// # Usage +/// +/// The [`EntityEvent`] trait can be derived. The `event` attribute can be used to further configure +/// the propagation behavior: adding `auto_propagate` sets [`EntityEvent::AUTO_PROPAGATE`] to `true`, +/// while adding `traversal = X` sets [`EntityEvent::Traversal`] to be of type `X`. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. +/// #[derive(Event, EntityEvent)] +/// #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// struct Damage { +/// amount: f32, +/// } +/// ``` +/// +/// An [`Observer`] can then be added to listen for this event type for the desired entity: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// // Spawn an enemy entity. +/// let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// +/// // Spawn some armor as a child of the enemy entity. +/// // When the armor takes damage, it will bubble the event up to the enemy, +/// // which can then handle the event with its own observer. +/// let armor_piece = world +/// .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// .observe(|trigger: On, mut query: Query<&mut Health>| { +/// // Note: `On::target` only exists because this is an `EntityEvent`. +/// let mut health = query.get_mut(trigger.target()).unwrap(); +/// health.0 -= trigger.amount; +/// }) +/// .id(); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger_targets`](World::trigger_targets) method, +/// providing the desired entity target(s): +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// # let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// # let armor_piece = world +/// # .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// # .observe(|trigger: On, mut query: Query<&mut Health>| { +/// # // Note: `On::target` only exists because this is an `EntityEvent`. +/// # let mut health = query.get_mut(trigger.target()).unwrap(); +/// # health.0 -= trigger.amount; +/// # }) +/// # .id(); +/// # +/// # world.flush(); +/// # +/// world.trigger_targets(Damage { amount: 10.0 }, armor_piece); +/// ``` +/// +/// [`World`]: crate::world::World +/// [`TriggerTargets`]: crate::observer::TriggerTargets +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `EntityEvent`", + label = "invalid `EntityEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, EntityEvent)]`" +)] +pub trait EntityEvent: Event { + /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. + /// + /// [`Entity`]: crate::entity::Entity + /// [propagation]: crate::observer::On::propagate + type Traversal: Traversal; + + /// When true, this event will always attempt to propagate when [triggered], without requiring a call + /// to [`On::propagate`]. + /// + /// [triggered]: crate::system::Commands::trigger_targets + /// [`On::propagate`]: crate::observer::On::propagate + const AUTO_PROPAGATE: bool = false; +} + +/// A buffered [`Event`] for pull-based event handling. +/// +/// Buffered events can be written with [`EventWriter`] and read using the [`EventReader`] system parameter. +/// These events are stored in the [`Events`] resource, and require periodically polling the world for new events, +/// typically in a system that runs as part of a schedule. +/// +/// While the polling imposes a small overhead, buffered events are useful for efficiently batch processing +/// a large number of events at once. This can make them more efficient than [`Event`]s used by [`Observer`]s +/// for events that happen at a high frequency or in large quantities. +/// +/// Unlike [`Event`]s triggered for observers, buffered events are evaluated at fixed points in the schedule +/// rather than immediately when they are sent. This allows for more predictable scheduling and deferring +/// event processing to a later point in time. +/// +/// Buffered events do *not* trigger observers automatically when they are written via an [`EventWriter`]. +/// However, they can still also be triggered on a [`World`] in case you want both buffered and immediate +/// event handling for the same event. +/// +/// Buffered events must be thread-safe. +/// +/// # Usage +/// +/// The [`BufferedEvent`] trait can be derived: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// #[derive(Event, BufferedEvent)] +/// struct Message(String); +/// ``` +/// +/// The event can then be written to the event buffer using an [`EventWriter`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn write_hello(mut writer: EventWriter) { +/// writer.write(Message("Hello!".to_string())); +/// } +/// ``` +/// +/// Buffered events can be efficiently read using an [`EventReader`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn read_messages(mut reader: EventReader) { +/// // Process all buffered events of type `Message`. +/// for Message(message) in reader.read() { +/// println!("{message}"); +/// } +/// } +/// ``` +/// +/// [`World`]: crate::world::World +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `BufferedEvent`", + label = "invalid `BufferedEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, BufferedEvent)]`" +)] +pub trait BufferedEvent: Event {} + /// An internal type that implements [`Component`] for a given [`Event`] type. /// /// This exists so we can easily get access to a unique [`ComponentId`] for each [`Event`] type, @@ -115,7 +354,7 @@ struct EventWrapperComponent(PhantomData); derive(Reflect), reflect(Clone, Debug, PartialEq, Hash) )] -pub struct EventId { +pub struct EventId { /// Uniquely identifies the event associated with this ID. // This value corresponds to the order in which each event was added to the world. pub id: usize, @@ -125,21 +364,21 @@ pub struct EventId { pub(super) _marker: PhantomData, } -impl Copy for EventId {} +impl Copy for EventId {} -impl Clone for EventId { +impl Clone for EventId { fn clone(&self) -> Self { *self } } -impl fmt::Display for EventId { +impl fmt::Display for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { ::fmt(self, f) } } -impl fmt::Debug for EventId { +impl fmt::Debug for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -150,27 +389,27 @@ impl fmt::Debug for EventId { } } -impl PartialEq for EventId { +impl PartialEq for EventId { fn eq(&self, other: &Self) -> bool { self.id == other.id } } -impl Eq for EventId {} +impl Eq for EventId {} -impl PartialOrd for EventId { +impl PartialOrd for EventId { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for EventId { +impl Ord for EventId { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } -impl Hash for EventId { +impl Hash for EventId { fn hash(&self, state: &mut H) { Hash::hash(&self.id, state); } @@ -178,7 +417,7 @@ impl Hash for EventId { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub(crate) struct EventInstance { +pub(crate) struct EventInstance { pub event_id: EventId, pub event: E, } diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 66447b7de4..7d1854149e 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::MaybeLocation, - event::{Event, EventCursor, EventId, EventInstance}, + event::{BufferedEvent, EventCursor, EventId, EventInstance}, resource::Resource, }; use core::{ @@ -38,10 +38,11 @@ use { /// dropped silently. /// /// # Example -/// ``` -/// use bevy_ecs::event::{Event, Events}; /// -/// #[derive(Event)] +/// ``` +/// use bevy_ecs::event::{BufferedEvent, Event, Events}; +/// +/// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize /// } @@ -91,7 +92,7 @@ use { /// [`event_update_system`]: super::event_update_system #[derive(Debug, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Default))] -pub struct Events { +pub struct Events { /// Holds the oldest still active events. /// Note that `a.start_event_count + a.len()` should always be equal to `events_b.start_event_count`. pub(crate) events_a: EventSequence, @@ -101,7 +102,7 @@ pub struct Events { } // Derived Default impl would incorrectly require E: Default -impl Default for Events { +impl Default for Events { fn default() -> Self { Self { events_a: Default::default(), @@ -111,7 +112,7 @@ impl Default for Events { } } -impl Events { +impl Events { /// Returns the index of the oldest event stored in the event buffer. pub fn oldest_event_count(&self) -> usize { self.events_a.start_event_count @@ -286,7 +287,7 @@ impl Events { } } -impl Extend for Events { +impl Extend for Events { #[track_caller] fn extend(&mut self, iter: I) where @@ -321,13 +322,13 @@ impl Extend for Events { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default))] -pub(crate) struct EventSequence { +pub(crate) struct EventSequence { pub(crate) events: Vec>, pub(crate) start_event_count: usize, } // Derived Default impl would incorrectly require E: Default -impl Default for EventSequence { +impl Default for EventSequence { fn default() -> Self { Self { events: Default::default(), @@ -336,7 +337,7 @@ impl Default for EventSequence { } } -impl Deref for EventSequence { +impl Deref for EventSequence { type Target = Vec>; fn deref(&self) -> &Self::Target { @@ -344,7 +345,7 @@ impl Deref for EventSequence { } } -impl DerefMut for EventSequence { +impl DerefMut for EventSequence { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.events } @@ -357,7 +358,7 @@ pub struct SendBatchIds { _marker: PhantomData, } -impl Iterator for SendBatchIds { +impl Iterator for SendBatchIds { type Item = EventId; fn next(&mut self) -> Option { @@ -377,7 +378,7 @@ impl Iterator for SendBatchIds { } } -impl ExactSizeIterator for SendBatchIds { +impl ExactSizeIterator for SendBatchIds { fn len(&self) -> usize { self.event_count.saturating_sub(self.last_count) } @@ -385,12 +386,11 @@ impl ExactSizeIterator for SendBatchIds { #[cfg(test)] mod tests { - use crate::event::Events; - use bevy_ecs_macros::Event; + use crate::event::{BufferedEvent, Event, Events}; #[test] fn iter_current_update_events_iterates_over_current_events() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut test_events = Events::::default(); diff --git a/crates/bevy_ecs/src/event/event_cursor.rs b/crates/bevy_ecs/src/event/event_cursor.rs index ff15ef4931..70e19a732c 100644 --- a/crates/bevy_ecs/src/event/event_cursor.rs +++ b/crates/bevy_ecs/src/event/event_cursor.rs @@ -1,5 +1,6 @@ use bevy_ecs::event::{ - Event, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, Events, + BufferedEvent, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, + Events, }; #[cfg(feature = "multi_threaded")] use bevy_ecs::event::{EventMutParIter, EventParIter}; @@ -19,9 +20,9 @@ use core::marker::PhantomData; /// /// ``` /// use bevy_ecs::prelude::*; -/// use bevy_ecs::event::{Event, Events, EventCursor}; +/// use bevy_ecs::event::{BufferedEvent, Events, EventCursor}; /// -/// #[derive(Event, Clone, Debug)] +/// #[derive(Event, BufferedEvent, Clone, Debug)] /// struct MyEvent; /// /// /// A system that both sends and receives events using a [`Local`] [`EventCursor`]. @@ -50,12 +51,12 @@ use core::marker::PhantomData; /// [`EventReader`]: super::EventReader /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventCursor { +pub struct EventCursor { pub(super) last_event_count: usize, pub(super) _marker: PhantomData, } -impl Default for EventCursor { +impl Default for EventCursor { fn default() -> Self { EventCursor { last_event_count: 0, @@ -64,7 +65,7 @@ impl Default for EventCursor { } } -impl Clone for EventCursor { +impl Clone for EventCursor { fn clone(&self) -> Self { EventCursor { last_event_count: self.last_event_count, @@ -73,7 +74,7 @@ impl Clone for EventCursor { } } -impl EventCursor { +impl EventCursor { /// See [`EventReader::read`](super::EventReader::read) pub fn read<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { self.read_with_id(events).without_id() diff --git a/crates/bevy_ecs/src/event/iterators.rs b/crates/bevy_ecs/src/event/iterators.rs index f9ee74b8b0..c90aed2a19 100644 --- a/crates/bevy_ecs/src/event/iterators.rs +++ b/crates/bevy_ecs/src/event/iterators.rs @@ -1,15 +1,15 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::Iter}; /// An iterator that yields any unread events from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIterator<'a, E: Event> { +pub struct EventIterator<'a, E: BufferedEvent> { iter: EventIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIterator<'a, E> { type Item = &'a E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -35,7 +35,7 @@ impl<'a, E: Event> Iterator for EventIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -43,13 +43,13 @@ impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { /// An iterator that yields any unread events (and their IDs) from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIteratorWithId<'a, E: Event> { +pub struct EventIteratorWithId<'a, E: BufferedEvent> { reader: &'a mut EventCursor, chain: Chain>, Iter<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -81,7 +81,7 @@ impl<'a, E: Event> EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIteratorWithId<'a, E> { type Item = (&'a E, EventId); fn next(&mut self) -> Option { match self @@ -131,16 +131,16 @@ impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[cfg(feature = "multi_threaded")] #[derive(Debug)] -pub struct EventParIter<'a, E: Event> { +pub struct EventParIter<'a, E: BufferedEvent> { reader: &'a mut EventCursor, slices: [&'a [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -149,7 +149,7 @@ pub struct EventParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventParIter<'a, E> { +impl<'a, E: BufferedEvent> EventParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -248,7 +248,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -260,7 +260,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventParIter<'a, E> { type IntoIter = EventIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 3bb422b7bb..fd624d1abf 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,8 +11,8 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{Event, EventId}; -pub use bevy_ecs_macros::Event; +pub use base::{BufferedEvent, EntityEvent, Event, EventId}; +pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; pub use collections::{Events, SendBatchIds}; pub use event_cursor::EventCursor; #[cfg(feature = "multi_threaded")] @@ -38,17 +38,20 @@ pub use writer::EventWriter; mod tests { use alloc::{vec, vec::Vec}; use bevy_ecs::{event::*, system::assert_is_read_only_system}; - use bevy_ecs_macros::Event; + use bevy_ecs_macros::BufferedEvent; - #[derive(Event, Copy, Clone, PartialEq, Eq, Debug)] + #[derive(Event, BufferedEvent, Copy, Clone, PartialEq, Eq, Debug)] struct TestEvent { i: usize, } - #[derive(Event, Clone, PartialEq, Debug, Default)] + #[derive(Event, BufferedEvent, Clone, PartialEq, Debug, Default)] struct EmptyTestEvent; - fn get_events(events: &Events, cursor: &mut EventCursor) -> Vec { + fn get_events( + events: &Events, + cursor: &mut EventCursor, + ) -> Vec { cursor.read(events).cloned().collect::>() } diff --git a/crates/bevy_ecs/src/event/mut_iterators.rs b/crates/bevy_ecs/src/event/mut_iterators.rs index 3cb531ce78..3fa8378f23 100644 --- a/crates/bevy_ecs/src/event/mut_iterators.rs +++ b/crates/bevy_ecs/src/event/mut_iterators.rs @@ -1,17 +1,17 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::IterMut}; /// An iterator that yields any unread events from an [`EventMutator`] or [`EventCursor`]. /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIterator<'a, E: Event> { +pub struct EventMutIterator<'a, E: BufferedEvent> { iter: EventMutIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIterator<'a, E> { type Item = &'a mut E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -37,7 +37,7 @@ impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -47,13 +47,13 @@ impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIteratorWithId<'a, E: Event> { +pub struct EventMutIteratorWithId<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, chain: Chain>, IterMut<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventMutIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -84,7 +84,7 @@ impl<'a, E: Event> EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIteratorWithId<'a, E> { type Item = (&'a mut E, EventId); fn next(&mut self) -> Option { match self @@ -134,16 +134,16 @@ impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[derive(Debug)] #[cfg(feature = "multi_threaded")] -pub struct EventMutParIter<'a, E: Event> { +pub struct EventMutParIter<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, slices: [&'a mut [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -152,7 +152,7 @@ pub struct EventMutParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> EventMutParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -251,7 +251,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -263,7 +263,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventMutParIter<'a, E> { type IntoIter = EventMutIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mutator.rs b/crates/bevy_ecs/src/event/mutator.rs index e95037af5b..a9c9459119 100644 --- a/crates/bevy_ecs/src/event/mutator.rs +++ b/crates/bevy_ecs/src/event/mutator.rs @@ -1,7 +1,7 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventMutParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, system::{Local, ResMut, SystemParam}, }; @@ -15,7 +15,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event, Debug)] +/// #[derive(Event, BufferedEvent, Debug)] /// pub struct MyEvent(pub u32); // Custom event type. /// fn my_system(mut reader: EventMutator) { /// for event in reader.read() { @@ -42,13 +42,13 @@ use bevy_ecs::{ /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventMutator<'w, 's, E: Event> { +pub struct EventMutator<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, 's, E: Event> EventMutator<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventMutator<'w, 's, E> { /// Iterates over the events this [`EventMutator`] has not seen yet. This updates the /// [`EventMutator`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -69,7 +69,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -116,7 +116,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventMutator) { diff --git a/crates/bevy_ecs/src/event/reader.rs b/crates/bevy_ecs/src/event/reader.rs index 995e2ca9e9..e15b3ea9e7 100644 --- a/crates/bevy_ecs/src/event/reader.rs +++ b/crates/bevy_ecs/src/event/reader.rs @@ -1,11 +1,11 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventIterator, EventIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventIterator, EventIteratorWithId, Events}, system::{Local, Res, SystemParam}, }; -/// Reads events of type `T` in order and tracks which events have already been read. +/// Reads [`BufferedEvent`]s of type `T` in order and tracks which events have already been read. /// /// # Concurrency /// @@ -14,13 +14,13 @@ use bevy_ecs::{ /// /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventReader<'w, 's, E: Event> { +pub struct EventReader<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: Res<'w, Events>, } -impl<'w, 's, E: Event> EventReader<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventReader<'w, 's, E> { /// Iterates over the events this [`EventReader`] has not seen yet. This updates the /// [`EventReader`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -41,7 +41,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -88,7 +88,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventReader) { diff --git a/crates/bevy_ecs/src/event/registry.rs b/crates/bevy_ecs/src/event/registry.rs index 231f792f68..7889de62da 100644 --- a/crates/bevy_ecs/src/event/registry.rs +++ b/crates/bevy_ecs/src/event/registry.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::{DetectChangesMut, MutUntyped}, component::{ComponentId, Tick}, - event::{Event, Events}, + event::{BufferedEvent, Events}, resource::Resource, world::World, }; @@ -45,7 +45,7 @@ impl EventRegistry { /// /// If no instance of the [`EventRegistry`] exists in the world, this will add one - otherwise it will use /// the existing instance. - pub fn register_event(world: &mut World) { + pub fn register_event(world: &mut World) { // By initializing the resource here, we can be sure that it is present, // and receive the correct, up-to-date `ComponentId` even if it was previously removed. let component_id = world.init_resource::>(); @@ -81,8 +81,8 @@ impl EventRegistry { } } - /// Removes an event from the world and it's associated [`EventRegistry`]. - pub fn deregister_events(world: &mut World) { + /// Removes an event from the world and its associated [`EventRegistry`]. + pub fn deregister_events(world: &mut World) { let component_id = world.init_resource::>(); let mut registry = world.get_resource_or_init::(); registry diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index 5854ab34fb..4c38401eb4 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -1,9 +1,9 @@ use bevy_ecs::{ - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EventId, Events, SendBatchIds}, system::{ResMut, SystemParam}, }; -/// Sends events of type `T`. +/// Sends [`BufferedEvent`]s of type `T`. /// /// # Usage /// @@ -11,7 +11,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// pub struct MyEvent; // Custom event type. /// fn my_system(mut writer: EventWriter) { /// writer.write(MyEvent); @@ -21,8 +21,8 @@ use bevy_ecs::{ /// ``` /// # Observers /// -/// "Buffered" Events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically -/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will +/// "Buffered" events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically +/// trigger any [`Observer`]s watching for that event, as each [`BufferedEvent`] has different requirements regarding _if_ it will /// be triggered, and if so, _when_ it will be triggered in the schedule. /// /// # Concurrency @@ -38,7 +38,7 @@ use bevy_ecs::{ /// /// ``` /// # use bevy_ecs::{prelude::*, event::Events}; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # pub struct MyEvent; /// fn send_untyped(mut commands: Commands) { /// // Send an event of a specific type without having to declare that @@ -59,12 +59,12 @@ use bevy_ecs::{ /// /// [`Observer`]: crate::observer::Observer #[derive(SystemParam)] -pub struct EventWriter<'w, E: Event> { - #[system_param(validation_message = "Event not initialized")] +pub struct EventWriter<'w, E: BufferedEvent> { + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, E: Event> EventWriter<'w, E> { +impl<'w, E: BufferedEvent> EventWriter<'w, E> { /// Writes an `event`, which can later be read by [`EventReader`](super::EventReader)s. /// This method returns the [ID](`EventId`) of the written `event`. /// diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 158219d547..d99e89b355 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -10,8 +10,9 @@ use crate::reflect::{ReflectComponent, ReflectFromWorld}; use crate::{ bundle::Bundle, - component::{Component, HookContext}, + component::Component, entity::Entity, + lifecycle::HookContext, relationship::{RelatedSpawner, RelatedSpawnerCommands}, system::EntityCommands, world::{DeferredWorld, EntityWorldMut, FromWorld, World}, @@ -19,9 +20,11 @@ use crate::{ use alloc::{format, string::String, vec::Vec}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::std_traits::ReflectDefault; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use bevy_utils::prelude::DebugName; use core::ops::Deref; use core::slice; -use disqualified::ShortName; use log::warn; /// Stores the parent entity of this child entity with this component. @@ -96,9 +99,14 @@ use log::warn; feature = "bevy_reflect", reflect(Component, PartialEq, Debug, FromWorld, Clone) )] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] #[relationship(relationship_target = Children)] #[doc(alias = "IsChild", alias = "Parent")] -pub struct ChildOf(pub Entity); +pub struct ChildOf(#[entities] pub Entity); impl ChildOf { /// The parent entity of this child entity. @@ -286,6 +294,12 @@ impl<'w> EntityWorldMut<'w> { self.insert_related::(index, children) } + /// Insert child at specific index. + /// See also [`insert_related`](Self::insert_related). + pub fn insert_child(&mut self, index: usize, child: Entity) -> &mut Self { + self.insert_related::(index, &[child]) + } + /// Adds the given child to this entity /// See also [`add_related`](Self::add_related). pub fn add_child(&mut self, child: Entity) -> &mut Self { @@ -297,6 +311,11 @@ impl<'w> EntityWorldMut<'w> { self.remove_related::(children) } + /// Removes the relationship between this entity and the given entity. + pub fn remove_child(&mut self, child: Entity) -> &mut Self { + self.remove_related::(&[child]) + } + /// Replaces all the related children with a new set of children. pub fn replace_children(&mut self, children: &[Entity]) -> &mut Self { self.replace_related::(children) @@ -366,6 +385,12 @@ impl<'a> EntityCommands<'a> { self.insert_related::(index, children) } + /// Insert children at specific index. + /// See also [`insert_related`](Self::insert_related). + pub fn insert_child(&mut self, index: usize, child: Entity) -> &mut Self { + self.insert_related::(index, &[child]) + } + /// Adds the given child to this entity pub fn add_child(&mut self, child: Entity) -> &mut Self { self.add_related::(&[child]) @@ -376,6 +401,11 @@ impl<'a> EntityCommands<'a> { self.remove_related::(children) } + /// Removes the relationship between this entity and the given entity. + pub fn remove_child(&mut self, child: Entity) -> &mut Self { + self.remove_related::(&[child]) + } + /// Replaces the children on this entity with a new list of children. pub fn replace_children(&mut self, children: &[Entity]) -> &mut Self { self.replace_related::(children) @@ -431,13 +461,14 @@ pub fn validate_parent_has_component( { // TODO: print name here once Name lives in bevy_ecs let name: Option = None; + let debug_name = DebugName::type_name::(); warn!( "warning[B0004]: {}{name} with the {ty_name} component has a parent without {ty_name}.\n\ - This will cause inconsistent behaviors! See: https://bevyengine.org/learn/errors/b0004", + This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", caller.map(|c| format!("{c}: ")).unwrap_or_default(), - ty_name = ShortName::of::(), + ty_name = debug_name.shortname(), name = name.map_or_else( - || format!("Entity {}", entity), + || format!("Entity {entity}"), |s| format!("The {s} entity") ), ); @@ -633,6 +664,29 @@ mod tests { ); } + #[test] + fn insert_child() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut entity_world_mut = world.spawn_empty(); + + let first_children = entity_world_mut.add_children(&[child1, child2]); + + let root = first_children.insert_child(1, child3).id(); + + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with( + root, + vec![Node::new(child1), Node::new(child3), Node::new(child2)] + ) + ); + } + // regression test for https://github.com/bevyengine/bevy/pull/19134 #[test] fn insert_children_index_bound() { @@ -690,6 +744,25 @@ mod tests { ); } + #[test] + fn remove_child() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut root = world.spawn_empty(); + root.add_children(&[child1, child2, child3]); + root.remove_child(child2); + let root = root.id(); + + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with(root, vec![Node::new(child1), Node::new(child3)]) + ); + } + #[test] fn self_parenting_invalid() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/label.rs b/crates/bevy_ecs/src/label.rs index c404c563bd..9d3f6f838d 100644 --- a/crates/bevy_ecs/src/label.rs +++ b/crates/bevy_ecs/src/label.rs @@ -12,9 +12,6 @@ pub use alloc::boxed::Box; /// An object safe version of [`Eq`]. This trait is automatically implemented /// for any `'static` type that implements `Eq`. pub trait DynEq: Any { - /// Casts the type to `dyn Any`. - fn as_any(&self) -> &dyn Any; - /// This method tests for `self` and `other` values to be equal. /// /// Implementers should avoid returning `true` when the underlying types are @@ -29,12 +26,8 @@ impl DynEq for T where T: Any + Eq, { - fn as_any(&self) -> &dyn Any { - self - } - fn dyn_eq(&self, other: &dyn DynEq) -> bool { - if let Some(other) = other.as_any().downcast_ref::() { + if let Some(other) = (other as &dyn Any).downcast_ref::() { return self == other; } false @@ -44,9 +37,6 @@ where /// An object safe version of [`Hash`]. This trait is automatically implemented /// for any `'static` type that implements `Hash`. pub trait DynHash: DynEq { - /// Casts the type to `dyn Any`. - fn as_dyn_eq(&self) -> &dyn DynEq; - /// Feeds this value into the given [`Hasher`]. fn dyn_hash(&self, state: &mut dyn Hasher); } @@ -58,10 +48,6 @@ impl DynHash for T where T: DynEq + Hash, { - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - fn dyn_hash(&self, mut state: &mut dyn Hasher) { T::hash(self, &mut state); self.type_id().hash(&mut state); @@ -120,7 +106,7 @@ macro_rules! define_label { ) => { $(#[$label_attr])* - pub trait $label_trait_name: 'static + Send + Sync + ::core::fmt::Debug { + pub trait $label_trait_name: Send + Sync + ::core::fmt::Debug + $crate::label::DynEq + $crate::label::DynHash { $($trait_extra_methods)* @@ -129,12 +115,6 @@ macro_rules! define_label { ///`. fn dyn_clone(&self) -> $crate::label::Box; - /// Casts this value to a form where it can be compared with other type-erased values. - fn as_dyn_eq(&self) -> &dyn $crate::label::DynEq; - - /// Feeds this value into the given [`Hasher`]. - fn dyn_hash(&self, state: &mut dyn ::core::hash::Hasher); - /// Returns an [`Interned`] value corresponding to `self`. fn intern(&self) -> $crate::intern::Interned where Self: Sized { @@ -151,15 +131,6 @@ macro_rules! define_label { (**self).dyn_clone() } - /// Casts this value to a form where it can be compared with other type-erased values. - fn as_dyn_eq(&self) -> &dyn $crate::label::DynEq { - (**self).as_dyn_eq() - } - - fn dyn_hash(&self, state: &mut dyn ::core::hash::Hasher) { - (**self).dyn_hash(state); - } - fn intern(&self) -> Self { *self } @@ -167,7 +138,7 @@ macro_rules! define_label { impl PartialEq for dyn $label_trait_name { fn eq(&self, other: &Self) -> bool { - self.as_dyn_eq().dyn_eq(other.as_dyn_eq()) + self.dyn_eq(other) } } @@ -188,7 +159,7 @@ macro_rules! define_label { use ::core::ptr; // Test that both the type id and pointer address are equivalent. - self.as_dyn_eq().type_id() == other.as_dyn_eq().type_id() + self.type_id() == other.type_id() && ptr::addr_eq(ptr::from_ref::(self), ptr::from_ref::(other)) } @@ -196,7 +167,7 @@ macro_rules! define_label { use ::core::{hash::Hash, ptr}; // Hash the type id... - self.as_dyn_eq().type_id().hash(state); + self.type_id().hash(state); // ...and the pointer address. // Cast to a unit `()` first to discard any pointer metadata. diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 0a2e1862dd..86275cd87f 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -13,8 +13,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![expect(unsafe_code, reason = "Unsafe code is used to improve performance.")] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -41,6 +41,7 @@ pub mod event; pub mod hierarchy; pub mod intern; pub mod label; +pub mod lifecycle; pub mod name; pub mod never; pub mod observer; @@ -48,7 +49,6 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod relationship; -pub mod removal_detection; pub mod resource; pub mod schedule; pub mod spawn; @@ -59,11 +59,18 @@ pub mod world; pub use bevy_ptr as ptr; +#[cfg(feature = "hotpatching")] +use event::{BufferedEvent, Event}; + /// The ECS prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] + #[expect( + deprecated, + reason = "`Trigger` was deprecated in favor of `On`, and `OnX` lifecycle events were deprecated in favor of `X` events." + )] pub use crate::{ bundle::Bundle, change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, @@ -71,18 +78,23 @@ pub mod prelude { component::Component, entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, - event::{Event, EventMutator, EventReader, EventWriter, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventMutator, EventReader, EventWriter, Events, + }, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, + lifecycle::{ + Add, Despawn, Insert, OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, Remove, + RemovedComponents, Replace, + }, name::{Name, NameOrEntity}, - observer::{Observer, Trigger}, + observer::{Observer, On, Trigger}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, - removal_detection::RemovedComponents, resource::Resource, schedule::{ - common_conditions::*, ApplyDeferred, Condition, IntoScheduleConfigs, IntoSystemSet, - Schedule, Schedules, SystemSet, + common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, + Schedules, SystemCondition, SystemSet, }, spawn::{Spawn, SpawnRelated}, system::{ @@ -93,7 +105,7 @@ pub mod prelude { }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, + FromWorld, World, }, }; @@ -123,6 +135,13 @@ pub mod __macro_exports { pub use alloc::vec::Vec; } +/// Event sent when a hotpatch happens. +/// +/// Systems should refresh their inner pointers. +#[cfg(feature = "hotpatching")] +#[derive(Event, BufferedEvent, Default)] +pub struct HotPatched; + #[cfg(test)] mod tests { use crate::{ @@ -1228,7 +1247,6 @@ mod tests { .components() .get_resource_id(TypeId::of::()) .unwrap(); - let archetype_component_id = world.storages().resources.get(resource_id).unwrap().id(); assert_eq!(world.resource::().0, 123); assert!(world.contains_resource::()); @@ -1290,14 +1308,6 @@ mod tests { resource_id, current_resource_id, "resource id does not change after removing / re-adding" ); - - let current_archetype_component_id = - world.storages().resources.get(resource_id).unwrap().id(); - - assert_eq!( - archetype_component_id, current_archetype_component_id, - "resource archetype component id does not change after removing / re-adding" - ); } #[test] @@ -1562,9 +1572,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread" - )] + #[should_panic] fn non_send_resource_drop_from_different_thread() { let mut world = World::default(); world.insert_non_send_resource(NonSendA::default()); @@ -1629,7 +1637,7 @@ mod tests { assert_eq!(q1.iter(&world).len(), 1); assert_eq!(q2.iter(&world).len(), 1); - assert_eq!(world.entities().len(), 2); + assert_eq!(world.entity_count(), 2); world.clear_entities(); @@ -1644,7 +1652,7 @@ mod tests { "world should not contain sparse set components" ); assert_eq!( - world.entities().len(), + world.entity_count(), 0, "world should not have any entities" ); @@ -2579,7 +2587,7 @@ mod tests { } #[test] - #[should_panic = "Recursive required components detected: A → B → C → B\nhelp: If this is intentional, consider merging the components."] + #[should_panic] fn required_components_recursion_errors() { #[derive(Component, Default)] #[require(B)] @@ -2597,7 +2605,7 @@ mod tests { } #[test] - #[should_panic = "Recursive required components detected: A → A\nhelp: Remove require(A)."] + #[should_panic] fn required_components_self_errors() { #[derive(Component, Default)] #[require(A)] diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs new file mode 100644 index 0000000000..e92c6cc7f9 --- /dev/null +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -0,0 +1,643 @@ +//! This module contains various tools to allow you to react to component insertion or removal, +//! as well as entity spawning and despawning. +//! +//! There are four main ways to react to these lifecycle events: +//! +//! 1. Using component hooks, which act as inherent constructors and destructors for components. +//! 2. Using [observers], which are a user-extensible way to respond to events, including component lifecycle events. +//! 3. Using the [`RemovedComponents`] system parameter, which offers an event-style interface. +//! 4. Using the [`Added`] query filter, which checks each component to see if it has been added since the last time a system ran. +//! +//! [observers]: crate::observer +//! [`Added`]: crate::query::Added +//! +//! # Types of lifecycle events +//! +//! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered +//! when a component is added to an entity: +//! +//! - [`Add`]: Triggered when a component is added to an entity that did not already have it. +//! - [`Insert`]: Triggered when a component is added to an entity, regardless of whether it already had it. +//! +//! When both events occur, [`Add`] hooks are evaluated before [`Insert`]. +//! +//! Next, we have lifecycle events that are triggered when a component is removed from an entity: +//! +//! - [`Replace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. +//! - [`Remove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. +//! - [`Despawn`]: Triggered for each component on an entity when it is despawned. +//! +//! [`Replace`] hooks are evaluated before [`Remove`], then finally [`Despawn`] hooks are evaluated. +//! +//! [`Add`] and [`Remove`] are counterparts: they are only triggered when a component is added or removed +//! from an entity in such a way as to cause a change in the component's presence on that entity. +//! Similarly, [`Insert`] and [`Replace`] are counterparts: they are triggered when a component is added or replaced +//! on an entity, regardless of whether this results in a change in the component's presence on that entity. +//! +//! To reliably synchronize data structures using with component lifecycle events, +//! you can combine [`Insert`] and [`Replace`] to fully capture any changes to the data. +//! This is particularly useful in combination with immutable components, +//! to avoid any lifecycle-bypassing mutations. +//! +//! ## Lifecycle events and component types +//! +//! Despite the absence of generics, each lifecycle event is associated with a specific component. +//! When defining a component hook for a [`Component`] type, that component is used. +//! When listening to lifecycle events for observers, the `B: Bundle` generic is used. +//! +//! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], +//! which are assigned during [`World`] initialization. +//! For example, [`Add`] corresponds to [`ADD`]. +//! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths. +use crate::{ + change_detection::MaybeLocation, + component::{Component, ComponentId, ComponentIdFor, Tick}, + entity::Entity, + event::{ + BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator, + EventIteratorWithId, Events, + }, + query::FilteredAccessSet, + relationship::RelationshipHookMode, + storage::SparseSet, + system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, +}; + +use derive_more::derive::Into; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use core::{ + fmt::Debug, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, + option, +}; + +/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. +pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); + +/// Context provided to a [`ComponentHook`]. +#[derive(Clone, Copy, Debug)] +pub struct HookContext { + /// The [`Entity`] this hook was invoked for. + pub entity: Entity, + /// The [`ComponentId`] this hook was invoked for. + pub component_id: ComponentId, + /// The caller location is `Some` if the `track_caller` feature is enabled. + pub caller: MaybeLocation, + /// Configures how relationship hooks will run + pub relationship_hook_mode: RelationshipHookMode, +} + +/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. +/// +/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. +/// These are intended to be used for structural side effects that need to happen when a component is added or removed, +/// and are not intended for general-purpose logic. +/// +/// For example, you might use a hook to update a cached index when a component is added, +/// to clean up resources when a component is removed, +/// or to keep hierarchical data structures across entities in sync. +/// +/// This information is stored in the [`ComponentInfo`](crate::component::ComponentInfo) of the associated component. +/// +/// There are two ways of configuring hooks for a component: +/// 1. Defining the relevant hooks on the [`Component`] implementation +/// 2. Using the [`World::register_component_hooks`] method +/// +/// # Example +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_platform::collections::HashSet; +/// +/// #[derive(Component)] +/// struct MyTrackedComponent; +/// +/// #[derive(Resource, Default)] +/// struct TrackedEntities(HashSet); +/// +/// let mut world = World::new(); +/// world.init_resource::(); +/// +/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks +/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); +/// assert!(tracked_component_query.iter(&world).next().is_none()); +/// +/// world.register_component_hooks::().on_add(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.insert(context.entity); +/// }); +/// +/// world.register_component_hooks::().on_remove(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.remove(&context.entity); +/// }); +/// +/// let entity = world.spawn(MyTrackedComponent).id(); +/// let tracked_entities = world.resource::(); +/// assert!(tracked_entities.0.contains(&entity)); +/// +/// world.despawn(entity); +/// let tracked_entities = world.resource::(); +/// assert!(!tracked_entities.0.contains(&entity)); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ComponentHooks { + pub(crate) on_add: Option, + pub(crate) on_insert: Option, + pub(crate) on_replace: Option, + pub(crate) on_remove: Option, + pub(crate) on_despawn: Option, +} + +impl ComponentHooks { + pub(crate) fn update_from_component(&mut self) -> &mut Self { + if let Some(hook) = C::on_add() { + self.on_add(hook); + } + if let Some(hook) = C::on_insert() { + self.on_insert(hook); + } + if let Some(hook) = C::on_replace() { + self.on_replace(hook); + } + if let Some(hook) = C::on_remove() { + self.on_remove(hook); + } + if let Some(hook) = C::on_despawn() { + self.on_despawn(hook); + } + + self + } + + /// Register a [`ComponentHook`] that will be run when this component is added to an entity. + /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as + /// adding all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_add` hook + pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_add(hook) + .expect("Component already has an on_add hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// or replaced. + /// + /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_insert` hook + pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_insert(hook) + .expect("Component already has an on_insert hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, + /// such as being replaced (with `.insert`) or removed. + /// + /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, + /// allowing access to the previous data just before it is dropped. + /// This hook does *not* run if the entity did not already have this component. + /// + /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_replace` hook + pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_replace(hook) + .expect("Component already has an on_replace hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// Despawning an entity counts as removing all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_remove` hook + pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_remove(hook) + .expect("Component already has an on_remove hook") + } + + /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_despawn` hook + pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_despawn(hook) + .expect("Component already has an on_despawn hook") + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. + /// + /// This is a fallible version of [`Self::on_add`]. + /// + /// Returns `None` if the component already has an `on_add` hook. + pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_add.is_some() { + return None; + } + self.on_add = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// + /// This is a fallible version of [`Self::on_insert`]. + /// + /// Returns `None` if the component already has an `on_insert` hook. + pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_insert.is_some() { + return None; + } + self.on_insert = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed + /// + /// This is a fallible version of [`Self::on_replace`]. + /// + /// Returns `None` if the component already has an `on_replace` hook. + pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_replace.is_some() { + return None; + } + self.on_replace = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// + /// This is a fallible version of [`Self::on_remove`]. + /// + /// Returns `None` if the component already has an `on_remove` hook. + pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_remove.is_some() { + return None; + } + self.on_remove = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// This is a fallible version of [`Self::on_despawn`]. + /// + /// Returns `None` if the component already has an `on_despawn` hook. + pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_despawn.is_some() { + return None; + } + self.on_despawn = Some(hook); + Some(self) + } +} + +/// [`ComponentId`] for [`Add`] +pub const ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`Insert`] +pub const INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`Replace`] +pub const REPLACE: ComponentId = ComponentId::new(2); +/// [`ComponentId`] for [`Remove`] +pub const REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`Despawn`] +pub const DESPAWN: ComponentId = ComponentId::new(4); + +/// Trigger emitted when a component is inserted onto an entity that does not already have that +/// component. Runs before `Insert`. +/// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnAdd")] +pub struct Add; + +/// Trigger emitted when a component is inserted, regardless of whether or not the entity already +/// had that component. Runs after `Add`, if it ran. +/// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnInsert")] +pub struct Insert; + +/// Trigger emitted when a component is removed from an entity, regardless +/// of whether or not it is later replaced. +/// +/// Runs before the value is replaced, so you can still access the original component data. +/// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnReplace")] +pub struct Replace; + +/// Trigger emitted when a component is removed from an entity, and runs before the component is +/// removed, so you can still access the component data. +/// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnRemove")] +pub struct Remove; + +/// Trigger emitted for each component on an entity when it is despawned. +/// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnDespawn")] +pub struct Despawn; + +/// Deprecated in favor of [`Add`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Add`.")] +pub type OnAdd = Add; + +/// Deprecated in favor of [`Insert`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Insert`.")] +pub type OnInsert = Insert; + +/// Deprecated in favor of [`Replace`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Replace`.")] +pub type OnReplace = Replace; + +/// Deprecated in favor of [`Remove`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Remove`.")] +pub type OnRemove = Remove; + +/// Deprecated in favor of [`Despawn`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Despawn`.")] +pub type OnDespawn = Despawn; + +/// Wrapper around [`Entity`] for [`RemovedComponents`]. +/// Internally, `RemovedComponents` uses these as an `Events`. +#[derive(Event, BufferedEvent, Debug, Clone, Into)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] +pub struct RemovedComponentEntity(Entity); + +/// Wrapper around a [`EventCursor`] so that we +/// can differentiate events between components. +#[derive(Debug)] +pub struct RemovedComponentReader +where + T: Component, +{ + reader: EventCursor, + marker: PhantomData, +} + +impl Default for RemovedComponentReader { + fn default() -> Self { + Self { + reader: Default::default(), + marker: PhantomData, + } + } +} + +impl Deref for RemovedComponentReader { + type Target = EventCursor; + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +impl DerefMut for RemovedComponentReader { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reader + } +} + +/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. +#[derive(Default, Debug)] +pub struct RemovedComponentEvents { + event_sets: SparseSet>, +} + +impl RemovedComponentEvents { + /// Creates an empty storage buffer for component removal events. + pub fn new() -> Self { + Self::default() + } + + /// For each type of component, swaps the event buffers and clears the oldest event buffer. + /// In general, this should be called once per frame/update. + pub fn update(&mut self) { + for (_component_id, events) in self.event_sets.iter_mut() { + events.update(); + } + } + + /// Returns an iterator over components and their entity events. + pub fn iter(&self) -> impl Iterator)> { + self.event_sets.iter() + } + + /// Gets the event storage for a given component. + pub fn get( + &self, + component_id: impl Into, + ) -> Option<&Events> { + self.event_sets.get(component_id.into()) + } + + /// Sends a removal event for the specified component. + pub fn send(&mut self, component_id: impl Into, entity: Entity) { + self.event_sets + .get_or_insert_with(component_id.into(), Default::default) + .send(RemovedComponentEntity(entity)); + } +} + +/// A [`SystemParam`] that yields entities that had their `T` [`Component`] +/// removed or have been despawned with it. +/// +/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). +/// +/// Unlike hooks or observers (see the [lifecycle](crate) module docs), +/// this does not allow you to see which data existed before removal. +/// +/// If you are using `bevy_ecs` as a standalone crate, +/// note that the [`RemovedComponents`] list will not be automatically cleared for you, +/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). +/// +/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is +/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. +/// For the main world, this is delayed until after all `SubApp`s have run. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::lifecycle::RemovedComponents; +/// # +/// # #[derive(Component)] +/// # struct MyComponent; +/// fn react_on_removal(mut removed: RemovedComponents) { +/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); +/// } +/// # bevy_ecs::system::assert_is_system(react_on_removal); +/// ``` +#[derive(SystemParam)] +pub struct RemovedComponents<'w, 's, T: Component> { + component_id: ComponentIdFor<'s, T>, + reader: Local<'s, RemovedComponentReader>, + event_sets: &'w RemovedComponentEvents, +} + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIter<'a> = iter::Map< + iter::Flatten>>>, + fn(RemovedComponentEntity) -> Entity, +>; + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIterWithId<'a> = iter::Map< + iter::Flatten>>, + fn( + (&RemovedComponentEntity, EventId), + ) -> (Entity, EventId), +>; + +fn map_id_events( + (entity, id): (&RemovedComponentEntity, EventId), +) -> (Entity, EventId) { + (entity.clone().into(), id) +} + +// For all practical purposes, the api surface of `RemovedComponents` +// should be similar to `EventReader` to reduce confusion. +impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { + /// Fetch underlying [`EventCursor`]. + pub fn reader(&self) -> &EventCursor { + &self.reader + } + + /// Fetch underlying [`EventCursor`] mutably. + pub fn reader_mut(&mut self) -> &mut EventCursor { + &mut self.reader + } + + /// Fetch underlying [`Events`]. + pub fn events(&self) -> Option<&Events> { + self.event_sets.get(self.component_id.get()) + } + + /// Destructures to get a mutable reference to the `EventCursor` + /// and a reference to `Events`. + /// + /// This is necessary since Rust can't detect destructuring through methods and most + /// usecases of the reader uses the `Events` as well. + pub fn reader_mut_with_events( + &mut self, + ) -> Option<( + &mut RemovedComponentReader, + &Events, + )> { + self.event_sets + .get(self.component_id.get()) + .map(|events| (&mut *self.reader, events)) + } + + /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the + /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events + /// that happened before now. + pub fn read(&mut self) -> RemovedIter<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read(events).cloned()) + .into_iter() + .flatten() + .map(RemovedComponentEntity::into) + } + + /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. + pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read_with_id(events)) + .into_iter() + .flatten() + .map(map_id_events) + } + + /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. + pub fn len(&self) -> usize { + self.events() + .map(|events| self.reader.len(events)) + .unwrap_or(0) + } + + /// Returns `true` if there are no events available to read. + pub fn is_empty(&self) -> bool { + self.events() + .is_none_or(|events| self.reader.is_empty(events)) + } + + /// Consumes all available events. + /// + /// This means these events will not appear in calls to [`RemovedComponents::read()`] or + /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. + pub fn clear(&mut self) { + if let Some((reader, events)) = self.reader_mut_with_events() { + reader.clear(events); + } + } +} + +// SAFETY: Only reads World removed component events +unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} + +// SAFETY: no component value access. +unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { + type State = (); + type Item<'w, 's> = &'w RemovedComponentEvents; + + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } + + #[inline] + unsafe fn get_param<'w, 's>( + _state: &'s mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + world.removed_components() + } +} diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index cd2e946678..317c8f5017 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -141,7 +141,7 @@ pub struct NameOrEntity { pub entity: Entity, } -impl<'a> core::fmt::Display for NameOrEntityItem<'a> { +impl<'w, 's> core::fmt::Display for NameOrEntityItem<'w, 's> { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match self.name { @@ -159,6 +159,7 @@ impl From<&str> for Name { Name::new(name.to_owned()) } } + impl From for Name { #[inline(always)] fn from(name: String) -> Self { @@ -174,12 +175,14 @@ impl AsRef for Name { &self.name } } + impl From<&Name> for String { #[inline(always)] fn from(val: &Name) -> String { val.as_str().to_owned() } } + impl From for String { #[inline(always)] fn from(val: Name) -> String { @@ -274,9 +277,9 @@ mod tests { let e2 = world.spawn(name.clone()).id(); let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); - let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} assert_eq!(d1.to_string(), "0v0"); + let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); } diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs new file mode 100644 index 0000000000..e3fa6c530a --- /dev/null +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -0,0 +1,245 @@ +//! Centralized storage for observers, allowing for efficient look-ups. +//! +//! This has multiple levels: +//! - [`World::observers`] provides access to [`Observers`], which is a central storage for all observers. +//! - [`Observers`] contains multiple distinct caches in the form of [`CachedObservers`]. +//! - Most observers are looked up by the [`ComponentId`] of the event they are observing +//! - Lifecycle observers have their own fields to save lookups. +//! - [`CachedObservers`] contains maps of [`ObserverRunner`]s, which are the actual functions that will be run when the observer is triggered. +//! - These are split by target type, in order to allow for different lookup strategies. +//! - [`CachedComponentObservers`] is one of these maps, which contains observers that are specifically targeted at a component. + +use bevy_platform::collections::HashMap; + +use crate::{ + archetype::ArchetypeFlags, + change_detection::MaybeLocation, + component::ComponentId, + entity::EntityHashMap, + observer::{ObserverRunner, ObserverTrigger}, + prelude::*, + world::DeferredWorld, +}; + +/// An internal lookup table tracking all of the observers in the world. +/// +/// Stores a cache mapping trigger ids to the registered observers. +/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, +/// saving lookups for the most common triggers. +/// +/// This can be accessed via [`World::observers`]. +#[derive(Default, Debug)] +pub struct Observers { + // Cached ECS observers to save a lookup most common triggers. + add: CachedObservers, + insert: CachedObservers, + replace: CachedObservers, + remove: CachedObservers, + despawn: CachedObservers, + // Map from trigger type to set of observers listening to that trigger + cache: HashMap, +} + +impl Observers { + pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers { + use crate::lifecycle::*; + + match event_type { + ADD => &mut self.add, + INSERT => &mut self.insert, + REPLACE => &mut self.replace, + REMOVE => &mut self.remove, + DESPAWN => &mut self.despawn, + _ => self.cache.entry(event_type).or_default(), + } + } + + /// Attempts to get the observers for the given `event_type`. + /// + /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], + /// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module. + pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + use crate::lifecycle::*; + + match event_type { + ADD => Some(&self.add), + INSERT => Some(&self.insert), + REPLACE => Some(&self.replace), + REMOVE => Some(&self.remove), + DESPAWN => Some(&self.despawn), + _ => self.cache.get(&event_type), + } + } + + /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. + pub(crate) fn invoke( + mut world: DeferredWorld, + event_type: ComponentId, + current_target: Option, + original_target: Option, + components: impl Iterator + Clone, + data: &mut T, + propagate: &mut bool, + caller: MaybeLocation, + ) { + // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` + let (mut world, observers) = unsafe { + let world = world.as_unsafe_world_cell(); + // SAFETY: There are no outstanding world references + world.increment_trigger_id(); + let observers = world.observers(); + let Some(observers) = observers.try_get_observers(event_type) else { + return; + }; + // SAFETY: The only outstanding reference to world is `observers` + (world.into_deferred(), observers) + }; + + let trigger_for_components = components.clone(); + + let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { + (runner)( + world.reborrow(), + ObserverTrigger { + observer, + event_type, + components: components.clone().collect(), + current_target, + original_target, + caller, + }, + data.into(), + propagate, + ); + }; + // Trigger observers listening for any kind of this trigger + observers + .global_observers + .iter() + .for_each(&mut trigger_observer); + + // Trigger entity observers listening for this kind of trigger + if let Some(target_entity) = current_target { + if let Some(map) = observers.entity_observers.get(&target_entity) { + map.iter().for_each(&mut trigger_observer); + } + } + + // Trigger observers listening to this trigger targeting a specific component + trigger_for_components.for_each(|id| { + if let Some(component_observers) = observers.component_observers.get(&id) { + component_observers + .global_observers + .iter() + .for_each(&mut trigger_observer); + + if let Some(target_entity) = current_target { + if let Some(map) = component_observers + .entity_component_observers + .get(&target_entity) + { + map.iter().for_each(&mut trigger_observer); + } + } + } + }); + } + + pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + use crate::lifecycle::*; + + match event_type { + ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), + INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), + REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), + _ => None, + } + } + + pub(crate) fn update_archetype_flags( + &self, + component_id: ComponentId, + flags: &mut ArchetypeFlags, + ) { + if self.add.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); + } + + if self.insert.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); + } + + if self.replace.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); + } + + if self.remove.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); + } + + if self.despawn.component_observers.contains_key(&component_id) { + flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); + } + } +} + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event. +/// +/// This is stored inside of [`Observers`], specialized for each kind of observer. +#[derive(Default, Debug)] +pub struct CachedObservers { + // Observers listening for any time this event is fired, regardless of target + // This will also respond to events targeting specific components or entities + pub(super) global_observers: ObserverMap, + // Observers listening for this trigger fired at a specific component + pub(super) component_observers: HashMap, + // Observers listening for this trigger fired at a specific entity + pub(super) entity_observers: EntityHashMap, +} + +impl CachedObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific components or entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting components. + pub fn get_component_observers(&self) -> &HashMap { + &self.component_observers + } + + /// Returns the observers listening for this trigger targeting entities. + pub fn entity_observers(&self) -> &HashMap { + &self.component_observers + } +} + +/// Map between an observer entity and its [`ObserverRunner`] +pub type ObserverMap = EntityHashMap; + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component. +/// +/// This is stored inside of [`CachedObservers`]. +#[derive(Default, Debug)] +pub struct CachedComponentObservers { + // Observers listening to events targeting this component, but not a specific entity + pub(super) global_observers: ObserverMap, + // Observers listening to events targeting this component on a specific entity + pub(super) entity_component_observers: EntityHashMap, +} + +impl CachedComponentObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting this component on a specific entity. + pub fn entity_component_observers(&self) -> &EntityHashMap { + &self.entity_component_observers + } +} diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs new file mode 100644 index 0000000000..a9a3645121 --- /dev/null +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -0,0 +1,492 @@ +//! Information about observers that is stored on the entities themselves. +//! +//! This allows for easier cleanup, better inspection, and more flexible querying. +//! +//! Each observer is associated with an entity, defined by the [`Observer`] component. +//! The [`Observer`] component contains the system that will be run when the observer is triggered, +//! and the [`ObserverDescriptor`] which contains information about what the observer is observing. +//! +//! When we watch entities, we add the [`ObservedBy`] component to those entities, +//! which links back to the observer entity. + +use core::any::Any; + +use crate::{ + component::{ComponentCloneBehavior, ComponentId, Mutable, StorageType}, + entity::Entity, + error::{ErrorContext, ErrorHandler}, + lifecycle::{ComponentHook, HookContext}, + observer::{observer_system_runner, ObserverRunner}, + prelude::*, + system::{IntoObserverSystem, ObserverSystem}, + world::DeferredWorld, +}; +use alloc::boxed::Box; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; + +#[cfg(feature = "bevy_reflect")] +use crate::prelude::ReflectComponent; + +/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". +/// +/// Observers listen for a "trigger" of a specific [`Event`]. An event can be triggered on the [`World`] +/// by calling [`World::trigger`], or if the event is an [`EntityEvent`], it can also be triggered for specific +/// entity targets using [`World::trigger_targets`]. +/// +/// Note that [`BufferedEvent`]s sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. +/// They must be triggered at a specific point in the schedule. +/// +/// # Usage +/// +/// The simplest usage of the observer pattern looks like this: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// #[derive(Event)] +/// struct Speak { +/// message: String, +/// } +/// +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.event().message); +/// }); +/// +/// // Observers currently require a flush() to be registered. In the context of schedules, +/// // this will generally be done for you. +/// world.flush(); +/// +/// world.trigger(Speak { +/// message: "Hello!".into(), +/// }); +/// ``` +/// +/// Notice that we used [`World::add_observer`]. This is just a shorthand for spawning an [`Observer`] manually: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct Speak; +/// // These are functionally the same: +/// world.add_observer(|trigger: On| {}); +/// world.spawn(Observer::new(|trigger: On| {})); +/// ``` +/// +/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct PrintNames; +/// # #[derive(Component, Debug)] +/// # struct Name; +/// world.add_observer(|trigger: On, names: Query<&Name>| { +/// for name in &names { +/// println!("{name:?}"); +/// } +/// }); +/// ``` +/// +/// Note that [`On`] must always be the first parameter. +/// +/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct SpawnThing; +/// # #[derive(Component, Debug)] +/// # struct Thing; +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// commands.spawn(Thing); +/// }); +/// ``` +/// +/// Observers can also trigger new events: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # #[derive(Event)] +/// # struct A; +/// # #[derive(Event)] +/// # struct B; +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// commands.trigger(B); +/// }); +/// ``` +/// +/// When the commands are flushed (including these "nested triggers") they will be +/// recursively evaluated until there are no commands left, meaning nested triggers all +/// evaluate at the same time! +/// +/// If the event is an [`EntityEvent`], it can be triggered for specific entities, +/// which will be passed to the [`Observer`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// #[derive(Event, EntityEvent)] +/// struct Explode; +/// +/// world.add_observer(|trigger: On, mut commands: Commands| { +/// println!("Entity {} goes BOOM!", trigger.target()); +/// commands.entity(trigger.target()).despawn(); +/// }); +/// +/// world.flush(); +/// +/// world.trigger_targets(Explode, entity); +/// ``` +/// +/// You can trigger multiple entities at once: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// world.trigger_targets(Explode, [e1, e2]); +/// ``` +/// +/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component, Debug)] +/// # struct Name(String); +/// # let mut world = World::default(); +/// # let e1 = world.spawn_empty().id(); +/// # let e2 = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { +/// println!("Boom!"); +/// commands.entity(trigger.target()).despawn(); +/// }); +/// +/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { +/// println!("The explosion fizzles! This entity is immune!"); +/// }); +/// ``` +/// +/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. +/// This protects against observer "garbage" building up over time. +/// +/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) +/// just shorthand for spawning an [`Observer`] directly: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # let mut world = World::default(); +/// # let entity = world.spawn_empty().id(); +/// # #[derive(Event, EntityEvent)] +/// # struct Explode; +/// let mut observer = Observer::new(|trigger: On| {}); +/// observer.watch_entity(entity); +/// world.spawn(observer); +/// ``` +/// +/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! +/// +/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. +/// serves as the "source of truth" of the observer. +/// +/// [`SystemParam`]: crate::system::SystemParam +pub struct Observer { + hook_on_add: ComponentHook, + pub(crate) error_handler: Option, + pub(crate) system: Box, + pub(crate) descriptor: ObserverDescriptor, + pub(crate) last_trigger_id: u32, + pub(crate) despawned_watched_entities: u32, + pub(crate) runner: ObserverRunner, +} + +impl Observer { + /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered + /// for _any_ entity (or no entity). + /// + /// # Panics + /// + /// Panics if the given system is an exclusive system. + pub fn new>(system: I) -> Self { + let system = Box::new(IntoObserverSystem::into_system(system)); + assert!( + !system.is_exclusive(), + concat!( + "Exclusive system `{}` may not be used as observer.\n", + "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." + ), + system.name() + ); + Self { + system, + descriptor: Default::default(), + hook_on_add: hook_on_add::, + error_handler: None, + runner: observer_system_runner::, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer + pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { + Self { + system: Box::new(IntoSystem::into_system(|| {})), + descriptor: Default::default(), + hook_on_add: |mut world, hook_context| { + let default_error_handler = world.default_error_handler(); + world.commands().queue(move |world: &mut World| { + let entity = hook_context.entity; + if let Some(mut observe) = world.get_mut::(entity) { + if observe.descriptor.events.is_empty() { + return; + } + if observe.error_handler.is_none() { + observe.error_handler = Some(default_error_handler); + } + world.register_observer(entity); + } + }); + }, + error_handler: None, + runner, + despawned_watched_entities: 0, + last_trigger_id: 0, + } + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + pub fn with_entity(mut self, entity: Entity) -> Self { + self.descriptor.entities.push(entity); + self + } + + /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// for the `entity`. + /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. + pub fn watch_entity(&mut self, entity: Entity) { + self.descriptor.entities.push(entity); + } + + /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered + /// with the given component target. + pub fn with_component(mut self, component: ComponentId) -> Self { + self.descriptor.components.push(component); + self + } + + /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] + /// is triggered. + /// # Safety + /// The type of the `event` [`ComponentId`] _must_ match the actual value + /// of the event passed into the observer system. + pub unsafe fn with_event(mut self, event: ComponentId) -> Self { + self.descriptor.events.push(event); + self + } + + /// Set the error handler to use for this observer. + /// + /// See the [`error` module-level documentation](crate::error) for more information. + pub fn with_error_handler(mut self, error_handler: fn(BevyError, ErrorContext)) -> Self { + self.error_handler = Some(error_handler); + self + } + + /// Returns the [`ObserverDescriptor`] for this [`Observer`]. + pub fn descriptor(&self) -> &ObserverDescriptor { + &self.descriptor + } + + /// Returns the name of the [`Observer`]'s system . + pub fn system_name(&self) -> DebugName { + self.system.system_name() + } +} + +impl Component for Observer { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + type Mutability = Mutable; + fn on_add() -> Option { + Some(|world, context| { + let Some(observe) = world.get::(context.entity) else { + return; + }; + let hook = observe.hook_on_add; + hook(world, context); + }) + } + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let descriptor = core::mem::take( + &mut world + .entity_mut(entity) + .get_mut::() + .unwrap() + .as_mut() + .descriptor, + ); + world.commands().queue(move |world: &mut World| { + world.unregister_observer(entity, descriptor); + }); + }) + } +} + +/// Store information about what an [`Observer`] observes. +/// +/// This information is stored inside of the [`Observer`] component, +#[derive(Default, Clone)] +pub struct ObserverDescriptor { + /// The events the observer is watching. + pub(super) events: Vec, + + /// The components the observer is watching. + pub(super) components: Vec, + + /// The entities the observer is watching. + pub(super) entities: Vec, +} + +impl ObserverDescriptor { + /// Add the given `events` to the descriptor. + /// # Safety + /// The type of each [`ComponentId`] in `events` _must_ match the actual value + /// of the event passed into the observer. + pub unsafe fn with_events(mut self, events: Vec) -> Self { + self.events = events; + self + } + + /// Add the given `components` to the descriptor. + pub fn with_components(mut self, components: Vec) -> Self { + self.components = components; + self + } + + /// Add the given `entities` to the descriptor. + pub fn with_entities(mut self, entities: Vec) -> Self { + self.entities = entities; + self + } + + /// Returns the `events` that the observer is watching. + pub fn events(&self) -> &[ComponentId] { + &self.events + } + + /// Returns the `components` that the observer is watching. + pub fn components(&self) -> &[ComponentId] { + &self.components + } + + /// Returns the `entities` that the observer is watching. + pub fn entities(&self) -> &[Entity] { + &self.entities + } +} + +/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). +/// +/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters +/// erased. +/// +/// The type parameters of this function _must_ match those used to create the [`Observer`]. +/// As such, it is recommended to only use this function within the [`Observer::new`] method to +/// ensure type parameters match. +fn hook_on_add>( + mut world: DeferredWorld<'_>, + HookContext { entity, .. }: HookContext, +) { + world.commands().queue(move |world: &mut World| { + let event_id = E::register_component_id(world); + let mut components = alloc::vec![]; + B::component_ids(&mut world.components_registrator(), &mut |id| { + components.push(id); + }); + if let Some(mut observer) = world.get_mut::(entity) { + observer.descriptor.events.push(event_id); + observer.descriptor.components.extend(components); + + let system: &mut dyn Any = observer.system.as_mut(); + let system: *mut dyn ObserverSystem = system.downcast_mut::().unwrap(); + // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias + unsafe { + (*system).initialize(world); + } + world.register_observer(entity); + } + }); +} + +/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. +#[derive(Default, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Component, Debug))] +pub struct ObservedBy(pub(crate) Vec); + +impl ObservedBy { + /// Provides a read-only reference to the list of entities observing this entity. + pub fn get(&self) -> &[Entity] { + &self.0 + } +} + +impl Component for ObservedBy { + const STORAGE_TYPE: StorageType = StorageType::SparseSet; + type Mutability = Mutable; + + fn on_remove() -> Option { + Some(|mut world, HookContext { entity, .. }| { + let observed_by = { + let mut component = world.get_mut::(entity).unwrap(); + core::mem::take(&mut component.0) + }; + for e in observed_by { + let (total_entities, despawned_watched_entities) = { + let Ok(mut entity_mut) = world.get_entity_mut(e) else { + continue; + }; + let Some(mut state) = entity_mut.get_mut::() else { + continue; + }; + state.despawned_watched_entities += 1; + ( + state.descriptor.entities.len(), + state.despawned_watched_entities as usize, + ) + }; + + // Despawn Observer if it has no more active sources. + if total_entities == despawned_watched_entities { + world.commands().entity(e).despawn(); + } + } + }) + } + + fn clone_behavior() -> ComponentCloneBehavior { + ComponentCloneBehavior::Ignore + } +} + +pub(crate) trait AnyNamedSystem: Any + Send + Sync + 'static { + fn system_name(&self) -> DebugName; +} + +impl AnyNamedSystem for T { + fn system_name(&self) -> DebugName { + self.name() + } +} diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_cloning.rs similarity index 56% rename from crates/bevy_ecs/src/observer/entity_observer.rs rename to crates/bevy_ecs/src/observer/entity_cloning.rs index 2c2d42b1c9..ee37300e64 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -1,56 +1,14 @@ +//! Logic to track observers when cloning entities. + use crate::{ - component::{ - Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType, - }, - entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, + component::ComponentCloneBehavior, + entity::{ComponentCloneCtx, EntityClonerBuilder, EntityMapper, SourceComponent}, + observer::ObservedBy, world::World, }; -use alloc::vec::Vec; use super::Observer; -/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. -#[derive(Default)] -pub struct ObservedBy(pub(crate) Vec); - -impl Component for ObservedBy { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let observed_by = { - let mut component = world.get_mut::(entity).unwrap(); - core::mem::take(&mut component.0) - }; - for e in observed_by { - let (total_entities, despawned_watched_entities) = { - let Ok(mut entity_mut) = world.get_entity_mut(e) else { - continue; - }; - let Some(mut state) = entity_mut.get_mut::() else { - continue; - }; - state.despawned_watched_entities += 1; - ( - state.descriptor.entities.len(), - state.despawned_watched_entities as usize, - ) - }; - - // Despawn Observer if it has no more active sources. - if total_entities == despawned_watched_entities { - world.commands().entity(e).despawn(); - } - } - }) - } - - fn clone_behavior() -> ComponentCloneBehavior { - ComponentCloneBehavior::Ignore - } -} - impl EntityClonerBuilder<'_> { /// Sets the option to automatically add cloned entities to the observers targeting source entity. pub fn add_observers(&mut self, add_observers: bool) -> &mut Self { @@ -86,7 +44,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo let event_types = observer_state.descriptor.events.clone(); let components = observer_state.descriptor.components.clone(); for event_type in event_types { - let observers = world.observers.get_observers(event_type); + let observers = world.observers.get_observers_mut(event_type); if components.is_empty() { if let Some(map) = observers.entity_observers.get(&source).cloned() { observers.entity_observers.insert(target, map); @@ -97,8 +55,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo else { continue; }; - if let Some(map) = observers.entity_map.get(&source).cloned() { - observers.entity_map.insert(target, map); + if let Some(map) = + observers.entity_component_observers.get(&source).cloned() + { + observers.entity_component_observers.insert(target, map); } } } @@ -110,14 +70,18 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo #[cfg(test)] mod tests { use crate::{ - entity::EntityCloner, event::Event, observer::Trigger, resource::Resource, system::ResMut, + entity::EntityCloner, + event::{EntityEvent, Event}, + observer::On, + resource::Resource, + system::ResMut, world::World, }; #[derive(Resource, Default)] struct Num(usize); - #[derive(Event)] + #[derive(Event, EntityEvent)] struct E; #[test] @@ -127,7 +91,7 @@ mod tests { let e = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); world.flush(); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 2343e66aa1..e9036eee74 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1,541 +1,161 @@ -//! Types for creating and storing [`Observer`]s +//! Observers are a push-based tool for responding to [`Event`]s. +//! +//! ## Observer targeting +//! +//! Observers can be "global", listening for events that are both targeted at and not targeted at any specific entity, +//! or they can be "entity-specific", listening for events that are targeted at specific entities. +//! +//! They can also be further refined by listening to events targeted at specific components +//! (instead of using a generic event type), as is done with the [`Add`] family of lifecycle events. +//! +//! When entities are observed, they will receive an [`ObservedBy`] component, +//! which will be updated to track the observers that are currently observing them. +//! +//! Currently, [observers cannot be retargeted after spawning](https://github.com/bevyengine/bevy/issues/19587): +//! despawn and respawn an observer as a workaround. +//! +//! ## Writing observers +//! +//! Observers are systems which implement [`IntoObserverSystem`] that listen for [`Event`]s matching their +//! type and target(s). +//! To write observer systems, use [`On`] as the first parameter of your system. +//! This parameter provides access to the specific event that triggered the observer, +//! as well as the entity that the event was targeted at, if any. +//! +//! Observers can request other data from the world, such as via a [`Query`] or [`Res`]. +//! Commonly, you might want to verify that the entity that the observable event is targeting +//! has a specific component, or meets some other condition. [`Query::get`] or [`Query::contains`] +//! on the [`On::target`] entity is a good way to do this. +//! +//! [`Commands`] can also be used inside of observers. +//! This can be particularly useful for triggering other observers! +//! +//! ## Spawning observers +//! +//! Observers can be spawned via [`World::add_observer`], or the equivalent app method. +//! This will cause an entity with the [`Observer`] component to be created, +//! which will then run the observer system whenever the event it is watching is triggered. +//! +//! You can control the targets that an observer is watching by calling [`Observer::watch_entity`] +//! once the entity is spawned, or by manually spawning an entity with the [`Observer`] component +//! configured with the desired targets. +//! +//! Observers are fundamentally defined as "entities which have the [`Observer`] component" +//! allowing you to add it manually to existing entities. +//! At first, this seems convenient, but only one observer can be added to an entity at a time, +//! regardless of the event it responds to: like always, components are unique. +//! +//! Instead, a better way to achieve a similar aim is to +//! use the [`EntityWorldMut::observe`] / [`EntityCommands::observe`] method, +//! which spawns a new observer, and configures it to watch the entity it is called on. +//! Unfortunately, observers defined in this way +//! [currently cannot be spawned as part of bundles](https://github.com/bevyengine/bevy/issues/14204). +//! +//! ## Triggering observers +//! +//! Observers are most commonly triggered by [`Commands`], +//! via [`Commands::trigger`] (for untargeted [`Event`]s) or [`Commands::trigger_targets`] (for targeted [`EntityEvent`]s). +//! Like usual, equivalent methods are available on [`World`], allowing you to reduce overhead when working with exclusive world access. +//! +//! If your observer is configured to watch for a specific component or set of components instead, +//! you can pass in [`ComponentId`]s into [`Commands::trigger_targets`] by using the [`TriggerTargets`] trait. +//! As discussed in the [`On`] documentation, this use case is rare, and is currently only used +//! for [lifecycle](crate::lifecycle) events, which are automatically emitted. +//! +//! ## Observer bubbling +//! +//! When using an [`EntityEvent`] targeted at an entity, the event can optionally be propagated to other targets, +//! typically up to parents in an entity hierarchy. +//! +//! This behavior is controlled via [`EntityEvent::Traversal`] and [`EntityEvent::AUTO_PROPAGATE`], +//! with the details of the propagation path specified by the [`Traversal`](crate::traversal::Traversal) trait. +//! +//! When auto-propagation is enabled, propagation must be manually stopped to prevent the event from +//! continuing to other targets. This can be done using the [`On::propagate`] method inside of your observer. +//! +//! ## Observer timing +//! +//! Observers are triggered via [`Commands`], which imply that they are evaluated at the next sync point in the ECS schedule. +//! Accordingly, they have full access to the world, and are evaluated sequentially, in the order that the commands were sent. +//! +//! To control the relative ordering of observers sent from different systems, +//! order the systems in the schedule relative to each other. +//! +//! Currently, Bevy does not provide [a way to specify the ordering of observers](https://github.com/bevyengine/bevy/issues/14890) +//! listening to the same event relative to each other. +//! +//! Commands sent by observers are [currently not immediately applied](https://github.com/bevyengine/bevy/issues/19569). +//! Instead, all queued observers will run, and then all of the commands from those observers will be applied. +//! Careful use of [`Schedule::apply_deferred`] may help as a workaround. +//! +//! ## Lifecycle events and observers +//! +//! It is important to note that observers, just like [hooks](crate::lifecycle::ComponentHooks), +//! can listen to and respond to [lifecycle](crate::lifecycle) events. +//! Unlike hooks, observers are not treated as an "innate" part of component behavior: +//! they can be added or removed at runtime, and multiple observers +//! can be registered for the same lifecycle event for the same component. +//! +//! The ordering of hooks versus observers differs based on the lifecycle event in question: +//! +//! - when adding components, hooks are evaluated first, then observers +//! - when removing components, observers are evaluated first, then hooks +//! +//! This allows hooks to act as constructors and destructors for components, +//! as they always have the first and final say in the component's lifecycle. +//! +//! ## Cleaning up observers +//! +//! Currently, observer entities are never cleaned up, even if their target entity(s) are despawned. +//! This won't cause any runtime overhead, but is a waste of memory and can result in memory leaks. +//! +//! If you run into this problem, you could manually scan the world for observer entities and despawn them, +//! by checking if the entity in [`Observer::descriptor`] still exists. +//! +//! ## Observers vs buffered events +//! +//! By contrast, [`EventReader`] and [`EventWriter`] ("buffered events"), are pull-based. +//! They require periodically polling the world to check for new events, typically in a system that runs as part of a schedule. +//! +//! This imposes a small overhead, making observers a better choice for extremely rare events, +//! but buffered events can be more efficient for events that are expected to occur multiple times per frame, +//! as it allows for batch processing of events. +//! +//! The difference in timing is also an important consideration: +//! buffered events are evaluated at fixed points during schedules, +//! while observers are evaluated as soon as possible after the event is triggered. +//! +//! This provides more control over the timing of buffered event evaluation, +//! but allows for a more ad hoc approach with observers, +//! and enables indefinite chaining of observers triggering other observers (for both better and worse!). -mod entity_observer; +mod centralized_storage; +mod distributed_storage; +mod entity_cloning; mod runner; +mod system_param; +mod trigger_targets; -pub use entity_observer::ObservedBy; +pub use centralized_storage::*; +pub use distributed_storage::*; pub use runner::*; -use variadics_please::all_tuples; +pub use system_param::*; +pub use trigger_targets::*; use crate::{ - archetype::ArchetypeFlags, change_detection::MaybeLocation, component::ComponentId, - entity::EntityHashMap, prelude::*, system::IntoObserverSystem, world::{DeferredWorld, *}, }; -use alloc::vec::Vec; -use bevy_platform::collections::HashMap; -use bevy_ptr::Ptr; -use core::{ - fmt::Debug, - marker::PhantomData, - ops::{Deref, DerefMut}, -}; -use smallvec::SmallVec; - -/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the -/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also -/// contains event propagation information. See [`Trigger::propagate`] for more information. -pub struct Trigger<'w, E, B: Bundle = ()> { - event: &'w mut E, - propagate: &'w mut bool, - trigger: ObserverTrigger, - _marker: PhantomData, -} - -impl<'w, E, B: Bundle> Trigger<'w, E, B> { - /// Creates a new trigger for the given event and observer information. - pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { - Self { - event, - propagate, - trigger, - _marker: PhantomData, - } - } - - /// Returns the event type of this trigger. - pub fn event_type(&self) -> ComponentId { - self.trigger.event_type - } - - /// Returns a reference to the triggered event. - pub fn event(&self) -> &E { - self.event - } - - /// Returns a mutable reference to the triggered event. - pub fn event_mut(&mut self) -> &mut E { - self.event - } - - /// Returns a pointer to the triggered event. - pub fn event_ptr(&self) -> Ptr { - Ptr::from(&self.event) - } - - /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. It may - /// be [`Entity::PLACEHOLDER`]. - /// - /// Observable events can target specific entities. When those events fire, they will trigger - /// any observers on the targeted entities. In this case, the `target()` and `observer()` are - /// the same, because the observer that was triggered is attached to the entity that was - /// targeted by the event. - /// - /// However, it is also possible for those events to bubble up the entity hierarchy and trigger - /// observers on *different* entities, or trigger a global observer. In these cases, the - /// observing entity is *different* from the entity being targeted by the event. - /// - /// This is an important distinction: the entity reacting to an event is not always the same as - /// the entity triggered by the event. - pub fn target(&self) -> Entity { - self.trigger.target - } - - /// Returns the components that triggered the observer, out of the - /// components defined in `B`. Does not necessarily include all of them as - /// `B` acts like an `OR` filter rather than an `AND` filter. - pub fn components(&self) -> &[ComponentId] { - &self.trigger.components - } - - /// Returns the [`Entity`] that observed the triggered event. - /// This allows you to despawn the observer, ceasing observation. - /// - /// # Examples - /// - /// ```rust - /// # use bevy_ecs::prelude::{Commands, Trigger}; - /// # - /// # struct MyEvent { - /// # done: bool, - /// # } - /// # - /// /// Handle `MyEvent` and if it is done, stop observation. - /// fn my_observer(trigger: Trigger, mut commands: Commands) { - /// if trigger.event().done { - /// commands.entity(trigger.observer()).despawn(); - /// return; - /// } - /// - /// // ... - /// } - /// ``` - pub fn observer(&self) -> Entity { - self.trigger.observer - } - - /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. - /// - /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events - /// use `()` which ends the path immediately and prevents propagation. - /// - /// To enable propagation, you must: - /// + Set [`Event::Traversal`] to the component you want to propagate along. - /// + Either call `propagate(true)` in the first observer or set [`Event::AUTO_PROPAGATE`] to `true`. - /// - /// You can prevent an event from propagating further using `propagate(false)`. - /// - /// [`Traversal`]: crate::traversal::Traversal - pub fn propagate(&mut self, should_propagate: bool) { - *self.propagate = should_propagate; - } - - /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. - /// - /// [`propagate`]: Trigger::propagate - pub fn get_propagate(&self) -> bool { - *self.propagate - } - - /// Returns the source code location that triggered this observer. - pub fn caller(&self) -> MaybeLocation { - self.trigger.caller - } -} - -impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Trigger") - .field("event", &self.event) - .field("propagate", &self.propagate) - .field("trigger", &self.trigger) - .field("_marker", &self._marker) - .finish() - } -} - -impl<'w, E, B: Bundle> Deref for Trigger<'w, E, B> { - type Target = E; - - fn deref(&self) -> &Self::Target { - self.event - } -} - -impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.event - } -} - -/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`]. -/// -/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination -/// will run. -pub trait TriggerTargets { - /// The components the trigger should target. - fn components(&self) -> impl Iterator + Clone + '_; - - /// The entities the trigger should target. - fn entities(&self) -> impl Iterator + Clone + '_; -} - -impl TriggerTargets for &T { - fn components(&self) -> impl Iterator + Clone + '_ { - (**self).components() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - (**self).entities() - } -} - -impl TriggerTargets for Entity { - fn components(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } -} - -impl TriggerTargets for ComponentId { - fn components(&self) -> impl Iterator + Clone + '_ { - core::iter::once(*self) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - [].into_iter() - } -} - -impl TriggerTargets for Vec { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -impl TriggerTargets for [T; N] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -impl TriggerTargets for [T] { - fn components(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::components) - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - self.iter().flat_map(T::entities) - } -} - -macro_rules! impl_trigger_targets_tuples { - ($(#[$meta:meta])* $($trigger_targets: ident),*) => { - #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] - #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] - $(#[$meta])* - impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) - { - fn components(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.components()); - )* - iter - } - - fn entities(&self) -> impl Iterator + Clone + '_ { - let iter = [].into_iter(); - let ($($trigger_targets,)*) = self; - $( - let iter = iter.chain($trigger_targets.entities()); - )* - iter - } - } - } -} - -all_tuples!( - #[doc(fake_variadic)] - impl_trigger_targets_tuples, - 0, - 15, - T -); - -/// A description of what an [`Observer`] observes. -#[derive(Default, Clone)] -pub struct ObserverDescriptor { - /// The events the observer is watching. - events: Vec, - - /// The components the observer is watching. - components: Vec, - - /// The entities the observer is watching. - entities: Vec, -} - -impl ObserverDescriptor { - /// Add the given `events` to the descriptor. - /// # Safety - /// The type of each [`ComponentId`] in `events` _must_ match the actual value - /// of the event passed into the observer. - pub unsafe fn with_events(mut self, events: Vec) -> Self { - self.events = events; - self - } - - /// Add the given `components` to the descriptor. - pub fn with_components(mut self, components: Vec) -> Self { - self.components = components; - self - } - - /// Add the given `entities` to the descriptor. - pub fn with_entities(mut self, entities: Vec) -> Self { - self.entities = entities; - self - } - - /// Returns the `events` that the observer is watching. - pub fn events(&self) -> &[ComponentId] { - &self.events - } - - /// Returns the `components` that the observer is watching. - pub fn components(&self) -> &[ComponentId] { - &self.components - } - - /// Returns the `entities` that the observer is watching. - pub fn entities(&self) -> &[Entity] { - &self.entities - } -} - -/// Event trigger metadata for a given [`Observer`], -#[derive(Debug)] -pub struct ObserverTrigger { - /// The [`Entity`] of the observer handling the trigger. - pub observer: Entity, - /// The [`Event`] the trigger targeted. - pub event_type: ComponentId, - /// The [`ComponentId`]s the trigger targeted. - components: SmallVec<[ComponentId; 2]>, - /// The entity the trigger targeted. - pub target: Entity, - /// The location of the source code that triggered the observer. - pub caller: MaybeLocation, -} - -impl ObserverTrigger { - /// Returns the components that the trigger targeted. - pub fn components(&self) -> &[ComponentId] { - &self.components - } -} - -// Map between an observer entity and its runner -type ObserverMap = EntityHashMap; - -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. -#[derive(Default, Debug)] -pub struct CachedComponentObservers { - // Observers listening to triggers targeting this component - map: ObserverMap, - // Observers listening to triggers targeting this component on a specific entity - entity_map: EntityHashMap, -} - -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. -#[derive(Default, Debug)] -pub struct CachedObservers { - // Observers listening for any time this trigger is fired - map: ObserverMap, - // Observers listening for this trigger fired at a specific component - component_observers: HashMap, - // Observers listening for this trigger fired at a specific entity - entity_observers: EntityHashMap, -} - -/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers. -#[derive(Default, Debug)] -pub struct Observers { - // Cached ECS observers to save a lookup most common triggers. - on_add: CachedObservers, - on_insert: CachedObservers, - on_replace: CachedObservers, - on_remove: CachedObservers, - on_despawn: CachedObservers, - // Map from trigger type to set of observers - cache: HashMap, -} - -impl Observers { - pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { - match event_type { - ON_ADD => &mut self.on_add, - ON_INSERT => &mut self.on_insert, - ON_REPLACE => &mut self.on_replace, - ON_REMOVE => &mut self.on_remove, - ON_DESPAWN => &mut self.on_despawn, - _ => self.cache.entry(event_type).or_default(), - } - } - - pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { - match event_type { - ON_ADD => Some(&self.on_add), - ON_INSERT => Some(&self.on_insert), - ON_REPLACE => Some(&self.on_replace), - ON_REMOVE => Some(&self.on_remove), - ON_DESPAWN => Some(&self.on_despawn), - _ => self.cache.get(&event_type), - } - } - - /// This will run the observers of the given `event_type`, targeting the given `entity` and `components`. - pub(crate) fn invoke( - mut world: DeferredWorld, - event_type: ComponentId, - target: Entity, - components: impl Iterator + Clone, - data: &mut T, - propagate: &mut bool, - caller: MaybeLocation, - ) { - // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` - let (mut world, observers) = unsafe { - let world = world.as_unsafe_world_cell(); - // SAFETY: There are no outstanding world references - world.increment_trigger_id(); - let observers = world.observers(); - let Some(observers) = observers.try_get_observers(event_type) else { - return; - }; - // SAFETY: The only outstanding reference to world is `observers` - (world.into_deferred(), observers) - }; - - let trigger_for_components = components.clone(); - - let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| { - (runner)( - world.reborrow(), - ObserverTrigger { - observer, - event_type, - components: components.clone().collect(), - target, - caller, - }, - data.into(), - propagate, - ); - }; - // Trigger observers listening for any kind of this trigger - observers.map.iter().for_each(&mut trigger_observer); - - // Trigger entity observers listening for this kind of trigger - if target != Entity::PLACEHOLDER { - if let Some(map) = observers.entity_observers.get(&target) { - map.iter().for_each(&mut trigger_observer); - } - } - - // Trigger observers listening to this trigger targeting a specific component - trigger_for_components.for_each(|id| { - if let Some(component_observers) = observers.component_observers.get(&id) { - component_observers - .map - .iter() - .for_each(&mut trigger_observer); - - if target != Entity::PLACEHOLDER { - if let Some(map) = component_observers.entity_map.get(&target) { - map.iter().for_each(&mut trigger_observer); - } - } - } - }); - } - - pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { - match event_type { - ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), - ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), - ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), - ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), - ON_DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), - _ => None, - } - } - - pub(crate) fn update_archetype_flags( - &self, - component_id: ComponentId, - flags: &mut ArchetypeFlags, - ) { - if self.on_add.component_observers.contains_key(&component_id) { - flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); - } - - if self - .on_insert - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); - } - - if self - .on_replace - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); - } - - if self - .on_remove - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); - } - - if self - .on_despawn - .component_observers - .contains_key(&component_id) - { - flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); - } - } -} impl World { /// Spawns a "global" [`Observer`] which will watch for the given event. /// Returns its [`Entity`] as a [`EntityWorldMut`]. /// + /// `system` can be any system whose first parameter is [`On`]. + /// /// **Calling [`observe`](EntityWorldMut::observe) on the returned /// [`EntityWorldMut`] will observe the observer itself, which you very /// likely do not want.** @@ -548,10 +168,10 @@ impl World { /// struct A; /// /// # let mut world = World::new(); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); /// ``` @@ -580,7 +200,7 @@ impl World { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { - self.trigger_targets_dynamic_ref_with_caller(event_id, &mut event, (), caller); + self.trigger_dynamic_ref_with_caller(event_id, &mut event, caller); } } @@ -592,20 +212,41 @@ impl World { pub fn trigger_ref(&mut self, event: &mut E) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` - unsafe { self.trigger_targets_dynamic_ref(event_id, event, ()) }; + unsafe { self.trigger_dynamic_ref_with_caller(event_id, event, MaybeLocation::caller()) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + unsafe fn trigger_dynamic_ref_with_caller( + &mut self, + event_id: ComponentId, + event_data: &mut E, + caller: MaybeLocation, + ) { + let mut world = DeferredWorld::from(self); + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + world.trigger_observers_with_data::<_, ()>( + event_id, + None, + None, + core::iter::empty::(), + event_data, + false, + caller, + ); + }; + } + + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_targets_ref`] instead. #[track_caller] - pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { + pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { self.trigger_targets_with_caller(event, targets, MaybeLocation::caller()); } - pub(crate) fn trigger_targets_with_caller( + pub(crate) fn trigger_targets_with_caller( &mut self, mut event: E, targets: impl TriggerTargets, @@ -618,19 +259,23 @@ impl World { } } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. #[track_caller] - pub fn trigger_targets_ref(&mut self, event: &mut E, targets: impl TriggerTargets) { + pub fn trigger_targets_ref( + &mut self, + event: &mut E, + targets: impl TriggerTargets, + ) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. @@ -640,7 +285,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic( + pub unsafe fn trigger_targets_dynamic( &mut self, event_id: ComponentId, mut event_data: E, @@ -652,7 +297,7 @@ impl World { }; } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets_dynamic`], this method is most useful when it's necessary to check @@ -662,7 +307,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic_ref( + pub unsafe fn trigger_targets_dynamic_ref( &mut self, event_id: ComponentId, event_data: &mut E, @@ -679,7 +324,7 @@ impl World { /// # Safety /// /// See `trigger_targets_dynamic_ref` - unsafe fn trigger_targets_dynamic_ref_with_caller( + unsafe fn trigger_targets_dynamic_ref_with_caller( &mut self, event_id: ComponentId, event_data: &mut E, @@ -693,7 +338,8 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - Entity::PLACEHOLDER, + None, + None, targets.components(), event_data, false, @@ -706,7 +352,8 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - target_entity, + Some(target_entity), + Some(target_entity), targets.components(), event_data, E::AUTO_PROPAGATE, @@ -733,10 +380,12 @@ impl World { let descriptor = &observer_state.descriptor; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.insert(observer_entity, observer_state.runner); + cache + .global_observers + .insert(observer_entity, observer_state.runner); } else if descriptor.components.is_empty() { // Observer is not targeting any components so register it as an entity observer for &watched_entity in &observer_state.descriptor.entities { @@ -758,11 +407,16 @@ impl World { }); if descriptor.entities.is_empty() { // Register for all triggers targeting the component - observers.map.insert(observer_entity, observer_state.runner); + observers + .global_observers + .insert(observer_entity, observer_state.runner); } else { // Register for each watched entity for &watched_entity in &descriptor.entities { - let map = observers.entity_map.entry(watched_entity).or_default(); + let map = observers + .entity_component_observers + .entry(watched_entity) + .or_default(); map.insert(observer_entity, observer_state.runner); } } @@ -777,9 +431,9 @@ impl World { let observers = &mut self.observers; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.remove(&entity); + cache.global_observers.remove(&entity); } else if descriptor.components.is_empty() { for watched_entity in &descriptor.entities { // This check should be unnecessary since this observer hasn't been unregistered yet @@ -797,20 +451,24 @@ impl World { continue; }; if descriptor.entities.is_empty() { - observers.map.remove(&entity); + observers.global_observers.remove(&entity); } else { for watched_entity in &descriptor.entities { - let Some(map) = observers.entity_map.get_mut(watched_entity) else { + let Some(map) = + observers.entity_component_observers.get_mut(watched_entity) + else { continue; }; map.remove(&entity); if map.is_empty() { - observers.entity_map.remove(watched_entity); + observers.entity_component_observers.remove(watched_entity); } } } - if observers.map.is_empty() && observers.entity_map.is_empty() { + if observers.global_observers.is_empty() + && observers.entity_component_observers.is_empty() + { cache.component_observers.remove(component); if let Some(flag) = Observers::is_archetype_cached(event_type) { if let Some(by_component) = archetypes.by_component.get(component) { @@ -845,7 +503,7 @@ mod tests { use crate::component::ComponentId; use crate::{ change_detection::MaybeLocation, - observer::{Observer, OnReplace}, + observer::{Observer, Replace}, prelude::*, traversal::Traversal, }; @@ -863,10 +521,10 @@ mod tests { #[component(storage = "SparseSet")] struct S; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventA; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventWithData { counter: usize, } @@ -885,13 +543,13 @@ mod tests { struct ChildOf(Entity); impl Traversal for &'_ ChildOf { - fn traverse(item: Self::Item<'_>, _: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _: &D) -> Option { Some(item.0) } } - #[derive(Component, Event)] - #[event(traversal = &'static ChildOf, auto_propagate)] + #[derive(Component, Event, EntityEvent)] + #[entity_event(traversal = &'static ChildOf, auto_propagate)] struct EventPropagating; #[test] @@ -899,14 +557,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let entity = world.spawn(A).id(); world.despawn(entity); @@ -921,14 +577,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(A); @@ -945,14 +599,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(S); @@ -971,14 +623,12 @@ mod tests { let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut // and therefore does not automatically flush. @@ -995,25 +645,25 @@ mod tests { let mut world = World::new(); world.init_resource::(); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); commands.entity(obs.target()).insert(B); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); commands.entity(obs.target()).remove::(); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); commands.entity(obs.target()).remove::(); }, ); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_b"); }); @@ -1031,9 +681,9 @@ mod tests { fn observer_trigger_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 1); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 2); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 4); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 1); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 2); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 4); // This flush is required for the last observer to be called when triggering the event, // due to `World::add_observer` returning `WorldEntityMut`. world.flush(); @@ -1047,13 +697,13 @@ mod tests { fn observer_trigger_targets_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 1; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 2; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 4; }); // This flush is required for the last observer to be called when triggering the event, @@ -1071,24 +721,24 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_1")); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_2")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_1")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_2")); world.spawn(A).flush(); assert_eq!(vec!["add_2", "add_1"], world.resource::().0); // Our A entity plus our two observers - assert_eq!(world.entities().len(), 3); + assert_eq!(world.entity_count(), 3); } #[test] fn observer_multiple_events() { let mut world = World::new(); world.init_resource::(); - let on_remove = OnRemove::register_component_id(&mut world); + let on_remove = Remove::register_component_id(&mut world); world.spawn( - // SAFETY: OnAdd and OnRemove are both unit types, so this is safe + // SAFETY: Add and Remove are both unit types, so this is safe unsafe { - Observer::new(|_: Trigger, mut res: ResMut| { + Observer::new(|_: On, mut res: ResMut| { res.observed("add/remove"); }) .with_event(on_remove) @@ -1110,7 +760,7 @@ mod tests { world.register_component::(); world.register_component::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1124,7 +774,7 @@ mod tests { fn observer_despawn() { let mut world = World::new(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Observer triggered after being despawned."); }; let observer = world.add_observer(system).id(); @@ -1140,11 +790,11 @@ mod tests { let entity = world.spawn((A, B)).flush(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_a"); }); - let system: fn(Trigger) = |_: Trigger| { + let system: fn(On) = |_: On| { panic!("Observer triggered after being despawned."); }; @@ -1161,7 +811,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1174,11 +824,11 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); - world.add_observer(move |obs: Trigger, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), Entity::PLACEHOLDER); res.observed("event_a"); }); @@ -1196,16 +846,16 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) + .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); - world.add_observer(move |obs: Trigger, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), entity); res.observed("a_2"); }); @@ -1231,26 +881,26 @@ mod tests { // targets (entity_1, A) let entity_1 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); // targets (entity_2, B) let entity_2 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 10) + .observe(|_: On, mut res: ResMut| res.0 += 10) .id(); // targets any entity or component - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 100); + world.add_observer(|_: On, mut res: ResMut| res.0 += 100); // targets any entity, and components A or B - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 1000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 1000); // test all tuples - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 10000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 10000); world.add_observer( - |_: Trigger, mut res: ResMut| { + |_: On, mut res: ResMut| { res.0 += 100000; }, ); world.add_observer( - |_: Trigger, + |_: On, mut res: ResMut| res.0 += 1000000, ); @@ -1338,7 +988,7 @@ mod tests { let component_id = world.register_component::(); world.spawn( - Observer::new(|_: Trigger, mut res: ResMut| res.observed("event_a")) + Observer::new(|_: On, mut res: ResMut| res.observed("event_a")) .with_component(component_id), ); @@ -1358,7 +1008,7 @@ mod tests { fn observer_dynamic_trigger() { let mut world = World::new(); world.init_resource::(); - let event_a = OnRemove::register_component_id(&mut world); + let event_a = Remove::register_component_id(&mut world); // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { @@ -1382,21 +1032,27 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let parent = world - .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + let parent = world.spawn_empty().id(); + let child = world.spawn(ChildOf(parent)).id(); + + world.entity_mut(parent).observe( + move |trigger: On, mut res: ResMut| { res.observed("parent"); - }) - .id(); - let child = world - .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + assert_eq!(trigger.target(), parent); + assert_eq!(trigger.original_target(), child); + }, + ); + + world.entity_mut(child).observe( + move |trigger: On, mut res: ResMut| { res.observed("child"); - }) - .id(); + assert_eq!(trigger.target(), child); + assert_eq!(trigger.original_target(), child); + }, + ); - // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // TODO: ideally this flush is not necessary, but right now observe() returns EntityWorldMut // and therefore does not automatically flush. world.flush(); world.trigger_targets(EventPropagating, child); @@ -1411,14 +1067,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1441,14 +1097,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1471,7 +1127,7 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); @@ -1479,7 +1135,7 @@ mod tests { let child = world .spawn(ChildOf(parent)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child"); trigger.propagate(false); }, @@ -1501,21 +1157,21 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child_a = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_a"); }) .id(); let child_b = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1538,7 +1194,7 @@ mod tests { let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("event"); }) .id(); @@ -1558,7 +1214,7 @@ mod tests { let parent_a = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_a"); }) .id(); @@ -1566,7 +1222,7 @@ mod tests { let child_a = world .spawn(ChildOf(parent_a)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child_a"); trigger.propagate(false); }, @@ -1575,14 +1231,14 @@ mod tests { let parent_b = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_b"); }) .id(); let child_b = world .spawn(ChildOf(parent_b)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1603,7 +1259,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("event"); }); @@ -1625,7 +1281,7 @@ mod tests { world.init_resource::(); world.add_observer( - |trigger: Trigger, query: Query<&A>, mut res: ResMut| { + |trigger: On, query: Query<&A>, mut res: ResMut| { if query.get(trigger.target()).is_ok() { res.observed("event"); } @@ -1647,7 +1303,7 @@ mod tests { // Originally for https://github.com/bevyengine/bevy/issues/18452 #[test] fn observer_modifies_relationship() { - fn on_add(trigger: Trigger, mut commands: Commands) { + fn on_add(trigger: On, mut commands: Commands) { commands .entity(trigger.target()) .with_related_entities::(|rsc| { @@ -1668,7 +1324,7 @@ mod tests { let mut world = World::new(); // Observe the removal of A - this will run during despawn - world.add_observer(|_: Trigger, mut cmd: Commands| { + world.add_observer(|_: On, mut cmd: Commands| { // Spawn a new entity - this reserves a new ID and requires a flush // afterward before Entities::free can be called. cmd.spawn_empty(); @@ -1676,7 +1332,7 @@ mod tests { let ent = world.spawn(A).id(); - // Despawn our entity, which runs the OnRemove observer and allocates a + // Despawn our entity, which runs the Remove observer and allocates a // new Entity. // Should not panic - if it does, then Entities was not flushed properly // after the observer's spawn_empty. @@ -1694,7 +1350,7 @@ mod tests { let mut world = World::new(); // This fails because `ResA` is not present in the world - world.add_observer(|_: Trigger, _: Res, mut commands: Commands| { + world.add_observer(|_: On, _: Res, mut commands: Commands| { commands.insert_resource(ResB); }); world.trigger(EventA); @@ -1707,7 +1363,7 @@ mod tests { let mut world = World::new(); world.add_observer( - |_: Trigger, mut params: ParamSet<(Query, Commands)>| { + |_: On, mut params: ParamSet<(Query, Commands)>| { params.p1().insert_resource(ResA); }, ); @@ -1728,7 +1384,7 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.trigger(EventA); @@ -1742,10 +1398,10 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.commands().spawn(Component).clear(); @@ -1763,7 +1419,7 @@ mod tests { let b_id = world.register_component::(); world.add_observer( - |trigger: Trigger, mut counter: ResMut| { + |trigger: On, mut counter: ResMut| { for &component in trigger.components() { *counter.0.entry(component).or_default() += 1; } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index ac1caa5ad0..acc2830a7d 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,14 +1,10 @@ -use alloc::{boxed::Box, vec}; +//! Logic for evaluating observers, and storing functions inside of observers. + use core::any::Any; use crate::{ - component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, - error::{default_error_handler, ErrorContext}, - observer::{ObserverDescriptor, ObserverTrigger}, - prelude::*, - query::DebugCheckedUnwrap, - system::{IntoObserverSystem, ObserverSystem}, - world::DeferredWorld, + error::ErrorContext, observer::ObserverTrigger, prelude::*, query::DebugCheckedUnwrap, + system::ObserverSystem, world::DeferredWorld, }; use bevy_ptr::PtrMut; @@ -18,315 +14,7 @@ use bevy_ptr::PtrMut; /// but can be overridden for custom behavior. pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: &mut bool); -/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". -/// -/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`]. -/// -/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific -/// point in the schedule. -/// -/// # Usage -/// -/// The simplest usage -/// of the observer pattern looks like this: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// #[derive(Event)] -/// struct Speak { -/// message: String, -/// } -/// -/// world.add_observer(|trigger: Trigger| { -/// println!("{}", trigger.event().message); -/// }); -/// -/// // Observers currently require a flush() to be registered. In the context of schedules, -/// // this will generally be done for you. -/// world.flush(); -/// -/// world.trigger(Speak { -/// message: "Hello!".into(), -/// }); -/// ``` -/// -/// Notice that we used [`World::add_observer`]. This is just a shorthand for spawning an [`Observer`] manually: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct Speak; -/// // These are functionally the same: -/// world.add_observer(|trigger: Trigger| {}); -/// world.spawn(Observer::new(|trigger: Trigger| {})); -/// ``` -/// -/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct PrintNames; -/// # #[derive(Component, Debug)] -/// # struct Name; -/// world.add_observer(|trigger: Trigger, names: Query<&Name>| { -/// for name in &names { -/// println!("{name:?}"); -/// } -/// }); -/// ``` -/// -/// Note that [`Trigger`] must always be the first parameter. -/// -/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct SpawnThing; -/// # #[derive(Component, Debug)] -/// # struct Thing; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// commands.spawn(Thing); -/// }); -/// ``` -/// -/// Observers can also trigger new events: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # #[derive(Event)] -/// # struct A; -/// # #[derive(Event)] -/// # struct B; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// commands.trigger(B); -/// }); -/// ``` -/// -/// When the commands are flushed (including these "nested triggers") they will be -/// recursively evaluated until there are no commands left, meaning nested triggers all -/// evaluate at the same time! -/// -/// Events can be triggered for entities, which will be passed to the [`Observer`]: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let entity = world.spawn_empty().id(); -/// #[derive(Event)] -/// struct Explode; -/// -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// println!("Entity {} goes BOOM!", trigger.target()); -/// commands.entity(trigger.target()).despawn(); -/// }); -/// -/// world.flush(); -/// -/// world.trigger_targets(Explode, entity); -/// ``` -/// -/// You can trigger multiple entities at once: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let e1 = world.spawn_empty().id(); -/// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] -/// # struct Explode; -/// world.trigger_targets(Explode, [e1, e2]); -/// ``` -/// -/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # #[derive(Component, Debug)] -/// # struct Name(String); -/// # let mut world = World::default(); -/// # let e1 = world.spawn_empty().id(); -/// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] -/// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { -/// println!("Boom!"); -/// commands.entity(trigger.target()).despawn(); -/// }); -/// -/// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { -/// println!("The explosion fizzles! This entity is immune!"); -/// }); -/// ``` -/// -/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. -/// This protects against observer "garbage" building up over time. -/// -/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) -/// just shorthand for spawning an [`Observer`] directly: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # let mut world = World::default(); -/// # let entity = world.spawn_empty().id(); -/// # #[derive(Event)] -/// # struct Explode; -/// let mut observer = Observer::new(|trigger: Trigger| {}); -/// observer.watch_entity(entity); -/// world.spawn(observer); -/// ``` -/// -/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities! -/// -/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. -/// serves as the "source of truth" of the observer. -/// -/// [`SystemParam`]: crate::system::SystemParam -pub struct Observer { - hook_on_add: ComponentHook, - error_handler: Option, - system: Box, - pub(crate) descriptor: ObserverDescriptor, - pub(crate) last_trigger_id: u32, - pub(crate) despawned_watched_entities: u32, - pub(crate) runner: ObserverRunner, -} - -impl Observer { - /// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered - /// for _any_ entity (or no entity). - /// - /// # Panics - /// - /// Panics if the given system is an exclusive system. - pub fn new>(system: I) -> Self { - let system = Box::new(IntoObserverSystem::into_system(system)); - assert!( - !system.is_exclusive(), - concat!( - "Exclusive system `{}` may not be used as observer.\n", - "Instead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." - ), - system.name() - ); - Self { - system, - descriptor: Default::default(), - hook_on_add: hook_on_add::, - error_handler: None, - runner: observer_system_runner::, - despawned_watched_entities: 0, - last_trigger_id: 0, - } - } - - /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer - pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { - Self { - system: Box::new(|| {}), - descriptor: Default::default(), - hook_on_add: |mut world, hook_context| { - world.commands().queue(move |world: &mut World| { - let entity = hook_context.entity; - if let Some(mut observe) = world.get_mut::(entity) { - if observe.descriptor.events.is_empty() { - return; - } - if observe.error_handler.is_none() { - observe.error_handler = Some(default_error_handler()); - } - world.register_observer(entity); - } - }); - }, - error_handler: None, - runner, - despawned_watched_entities: 0, - last_trigger_id: 0, - } - } - - /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for the `entity`. - pub fn with_entity(mut self, entity: Entity) -> Self { - self.descriptor.entities.push(entity); - self - } - - /// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// for the `entity`. - /// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects. - pub fn watch_entity(&mut self, entity: Entity) { - self.descriptor.entities.push(entity); - } - - /// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered - /// with the given component target. - pub fn with_component(mut self, component: ComponentId) -> Self { - self.descriptor.components.push(component); - self - } - - /// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`] - /// is triggered. - /// # Safety - /// The type of the `event` [`ComponentId`] _must_ match the actual value - /// of the event passed into the observer system. - pub unsafe fn with_event(mut self, event: ComponentId) -> Self { - self.descriptor.events.push(event); - self - } - - /// Set the error handler to use for this observer. - /// - /// See the [`error` module-level documentation](crate::error) for more information. - pub fn with_error_handler(mut self, error_handler: fn(BevyError, ErrorContext)) -> Self { - self.error_handler = Some(error_handler); - self - } - - /// Returns the [`ObserverDescriptor`] for this [`Observer`]. - pub fn descriptor(&self) -> &ObserverDescriptor { - &self.descriptor - } -} - -impl Component for Observer { - const STORAGE_TYPE: StorageType = StorageType::SparseSet; - type Mutability = Mutable; - fn on_add() -> Option { - Some(|world, context| { - let Some(observe) = world.get::(context.entity) else { - return; - }; - let hook = observe.hook_on_add; - hook(world, context); - }) - } - fn on_remove() -> Option { - Some(|mut world, HookContext { entity, .. }| { - let descriptor = core::mem::take( - &mut world - .entity_mut(entity) - .get_mut::() - .unwrap() - .as_mut() - .descriptor, - ); - world.commands().queue(move |world: &mut World| { - world.unregister_observer(entity, descriptor); - }); - }) - } -} - -fn observer_system_runner>( +pub(super) fn observer_system_runner>( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, @@ -348,10 +36,8 @@ fn observer_system_runner>( return; } state.last_trigger_id = last_trigger; - // SAFETY: Observer was triggered so must have an `Observer` component. - let error_handler = unsafe { state.error_handler.debug_checked_unwrap() }; - let trigger: Trigger = Trigger::new( + let trigger: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, @@ -362,22 +48,29 @@ fn observer_system_runner>( // - observer was triggered so must have an `Observer` component. // - observer cannot be dropped or mutated until after the system pointer is already dropped. let system: *mut dyn ObserverSystem = unsafe { - let system = state.system.downcast_mut::().debug_checked_unwrap(); + let system: &mut dyn Any = state.system.as_mut(); + let system = system.downcast_mut::().debug_checked_unwrap(); &mut *system }; // SAFETY: - // - `update_archetype_component_access` is called first // - there are no outstanding references to world except a private component // - system is an `ObserverSystem` so won't mutate world beyond the access of a `DeferredWorld` // and is never exclusive // - system is the same type erased system from above unsafe { - (*system).update_archetype_component_access(world); + // Always refresh hotpatch pointers + // There's no guarantee that the `HotPatched` event would still be there once the observer is triggered. + #[cfg(feature = "hotpatching")] + (*system).refresh_hotpatch(); + match (*system).validate_param_unsafe(world) { Ok(()) => { if let Err(err) = (*system).run_unsafe(trigger, world) { - error_handler( + let handler = state + .error_handler + .unwrap_or_else(|| world.default_error_handler()); + handler( err, ErrorContext::Observer { name: (*system).name(), @@ -389,7 +82,10 @@ fn observer_system_runner>( } Err(e) => { if !e.skipped { - error_handler( + let handler = state + .error_handler + .unwrap_or_else(|| world.default_error_handler()); + handler( e.into(), ErrorContext::Observer { name: (*system).name(), @@ -402,44 +98,14 @@ fn observer_system_runner>( } } -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::component::ComponentHooks::on_add`). -/// -/// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters -/// erased. -/// -/// The type parameters of this function _must_ match those used to create the [`Observer`]. -/// As such, it is recommended to only use this function within the [`Observer::new`] method to -/// ensure type parameters match. -fn hook_on_add>( - mut world: DeferredWorld<'_>, - HookContext { entity, .. }: HookContext, -) { - world.commands().queue(move |world: &mut World| { - let event_id = E::register_component_id(world); - let mut components = vec![]; - B::component_ids(&mut world.components_registrator(), &mut |id| { - components.push(id); - }); - if let Some(mut observe) = world.get_mut::(entity) { - observe.descriptor.events.push(event_id); - observe.descriptor.components.extend(components); - - if observe.error_handler.is_none() { - observe.error_handler = Some(default_error_handler()); - } - let system: *mut dyn ObserverSystem = observe.system.downcast_mut::().unwrap(); - // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias - unsafe { - (*system).initialize(world); - } - world.register_observer(entity); - } - }); -} #[cfg(test)] mod tests { use super::*; - use crate::{event::Event, observer::Trigger}; + use crate::{ + error::{ignore, DefaultErrorHandler}, + event::Event, + observer::On, + }; #[derive(Event)] struct TriggerEvent; @@ -447,7 +113,7 @@ mod tests { #[test] #[should_panic(expected = "I failed!")] fn test_fallible_observer() { - fn system(_: Trigger) -> Result { + fn system(_: On) -> Result { Err("I failed!".into()) } @@ -462,26 +128,33 @@ mod tests { #[derive(Resource, Default)] struct Ran(bool); - fn system(_: Trigger, mut ran: ResMut) -> Result { + fn system(_: On, mut ran: ResMut) -> Result { ran.0 = true; Err("I failed!".into()) } + // Using observer error handler let mut world = World::default(); world.init_resource::(); - let observer = Observer::new(system).with_error_handler(crate::error::ignore); - world.spawn(observer); - Schedule::default().run(&mut world); + world.spawn(Observer::new(system).with_error_handler(ignore)); + world.trigger(TriggerEvent); + assert!(world.resource::().0); + + // Using world error handler + let mut world = World::default(); + world.init_resource::(); + world.spawn(Observer::new(system)); + // Test that the correct handler is used when the observer was added + // before the default handler + world.insert_resource(DefaultErrorHandler(ignore)); world.trigger(TriggerEvent); assert!(world.resource::().0); } #[test] - #[should_panic( - expected = "Exclusive system `bevy_ecs::observer::runner::tests::exclusive_system_cannot_be_observer::system` may not be used as observer.\nInstead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." - )] + #[should_panic] fn exclusive_system_cannot_be_observer() { - fn system(_: Trigger, _world: &mut World) {} + fn system(_: On, _world: &mut World) {} let mut world = World::default(); world.add_observer(system); } diff --git a/crates/bevy_ecs/src/observer/system_param.rs b/crates/bevy_ecs/src/observer/system_param.rs new file mode 100644 index 0000000000..27d6fef5b3 --- /dev/null +++ b/crates/bevy_ecs/src/observer/system_param.rs @@ -0,0 +1,206 @@ +//! System parameters for working with observers. + +use core::marker::PhantomData; +use core::ops::DerefMut; +use core::{fmt::Debug, ops::Deref}; + +use bevy_ptr::Ptr; +use smallvec::SmallVec; + +use crate::{ + bundle::Bundle, change_detection::MaybeLocation, component::ComponentId, event::EntityEvent, + prelude::*, +}; + +/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the +/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also +/// contains event propagation information. See [`On::propagate`] for more information. +/// +/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The entity involved *does not* have to have these components, but the observer will only be +/// triggered if the event matches the components in `B`. +/// +/// This is used to to avoid providing a generic argument in your event, as is done for [`Add`] +/// and the other lifecycle events. +/// +/// Providing multiple components in this bundle will cause this event to be triggered by any +/// matching component in the bundle, +/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). +pub struct On<'w, E, B: Bundle = ()> { + event: &'w mut E, + propagate: &'w mut bool, + trigger: ObserverTrigger, + _marker: PhantomData, +} + +/// Deprecated in favor of [`On`]. +#[deprecated(since = "0.17.0", note = "Renamed to `On`.")] +pub type Trigger<'w, E, B = ()> = On<'w, E, B>; + +impl<'w, E, B: Bundle> On<'w, E, B> { + /// Creates a new instance of [`On`] for the given event and observer information. + pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { + Self { + event, + propagate, + trigger, + _marker: PhantomData, + } + } + + /// Returns the event type of this [`On`] instance. + pub fn event_type(&self) -> ComponentId { + self.trigger.event_type + } + + /// Returns a reference to the triggered event. + pub fn event(&self) -> &E { + self.event + } + + /// Returns a mutable reference to the triggered event. + pub fn event_mut(&mut self) -> &mut E { + self.event + } + + /// Returns a pointer to the triggered event. + pub fn event_ptr(&self) -> Ptr { + Ptr::from(&self.event) + } + + /// Returns the components that triggered the observer, out of the + /// components defined in `B`. Does not necessarily include all of them as + /// `B` acts like an `OR` filter rather than an `AND` filter. + pub fn components(&self) -> &[ComponentId] { + &self.trigger.components + } + + /// Returns the [`Entity`] that observed the triggered event. + /// This allows you to despawn the observer, ceasing observation. + /// + /// # Examples + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(Event, EntityEvent)] + /// struct AssertEvent; + /// + /// fn assert_observer(trigger: On) { + /// assert_eq!(trigger.observer(), trigger.target()); + /// } + /// + /// let mut world = World::new(); + /// let observer = world.spawn(Observer::new(assert_observer)).id(); + /// + /// world.trigger_targets(AssertEvent, observer); + /// ``` + pub fn observer(&self) -> Entity { + self.trigger.observer + } + + /// Returns the source code location that triggered this observer. + pub fn caller(&self) -> MaybeLocation { + self.trigger.caller + } +} + +impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { + /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. + /// + /// Note that if event propagation is enabled, this may not be the same as the original target of the event, + /// which can be accessed via [`On::original_target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn target(&self) -> Entity { + self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. + /// + /// If event propagation is not enabled, this will always return the same value as [`On::target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn original_target(&self) -> Entity { + self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. + /// + /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events + /// use `()` which ends the path immediately and prevents propagation. + /// + /// To enable propagation, you must: + /// + Set [`EntityEvent::Traversal`] to the component you want to propagate along. + /// + Either call `propagate(true)` in the first observer or set [`EntityEvent::AUTO_PROPAGATE`] to `true`. + /// + /// You can prevent an event from propagating further using `propagate(false)`. + /// + /// [`Traversal`]: crate::traversal::Traversal + pub fn propagate(&mut self, should_propagate: bool) { + *self.propagate = should_propagate; + } + + /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. + /// + /// [`propagate`]: On::propagate + pub fn get_propagate(&self) -> bool { + *self.propagate + } +} + +impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("On") + .field("event", &self.event) + .field("propagate", &self.propagate) + .field("trigger", &self.trigger) + .field("_marker", &self._marker) + .finish() + } +} + +impl<'w, E, B: Bundle> Deref for On<'w, E, B> { + type Target = E; + + fn deref(&self) -> &Self::Target { + self.event + } +} + +impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.event + } +} + +/// Metadata about a specific [`Event`] that triggered an observer. +/// +/// This information is exposed via methods on [`On`]. +#[derive(Debug)] +pub struct ObserverTrigger { + /// The [`Entity`] of the observer handling the trigger. + pub observer: Entity, + /// The [`Event`] the trigger targeted. + pub event_type: ComponentId, + /// The [`ComponentId`]s the trigger targeted. + pub components: SmallVec<[ComponentId; 2]>, + /// The entity that the entity-event targeted, if any. + /// + /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. + pub current_target: Option, + /// The entity that the entity-event was originally targeted at, if any. + /// + /// If event propagation is enabled, this will be the first entity that the event was targeted at, + /// even if the event was propagated to other entities. + pub original_target: Option, + /// The location of the source code that triggered the observer. + pub caller: MaybeLocation, +} + +impl ObserverTrigger { + /// Returns the components that the trigger targeted. + pub fn components(&self) -> &[ComponentId] { + &self.components + } +} diff --git a/crates/bevy_ecs/src/observer/trigger_targets.rs b/crates/bevy_ecs/src/observer/trigger_targets.rs new file mode 100644 index 0000000000..77728e4acd --- /dev/null +++ b/crates/bevy_ecs/src/observer/trigger_targets.rs @@ -0,0 +1,117 @@ +//! Stores the [`TriggerTargets`] trait. + +use crate::{component::ComponentId, prelude::*}; +use alloc::vec::Vec; +use variadics_please::all_tuples; + +/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. +/// +/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific +/// event-target combination will run. +/// +/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. +/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, +/// allowing you to trigger events for multiple targets at once. +pub trait TriggerTargets { + /// The components the trigger should target. + fn components(&self) -> impl Iterator + Clone + '_; + + /// The entities the trigger should target. + fn entities(&self) -> impl Iterator + Clone + '_; +} + +impl TriggerTargets for &T { + fn components(&self) -> impl Iterator + Clone + '_ { + (**self).components() + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + (**self).entities() + } +} + +impl TriggerTargets for Entity { + fn components(&self) -> impl Iterator + Clone + '_ { + [].into_iter() + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + core::iter::once(*self) + } +} + +impl TriggerTargets for ComponentId { + fn components(&self) -> impl Iterator + Clone + '_ { + core::iter::once(*self) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + [].into_iter() + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +impl TriggerTargets for [T; N] { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +impl TriggerTargets for [T] { + fn components(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::components) + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + self.iter().flat_map(T::entities) + } +} + +macro_rules! impl_trigger_targets_tuples { + ($(#[$meta:meta])* $($trigger_targets: ident),*) => { + #[expect(clippy::allow_attributes, reason = "can't guarantee violation of non_snake_case")] + #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] + $(#[$meta])* + impl<$($trigger_targets: TriggerTargets),*> TriggerTargets for ($($trigger_targets,)*) + { + fn components(&self) -> impl Iterator + Clone + '_ { + let iter = [].into_iter(); + let ($($trigger_targets,)*) = self; + $( + let iter = iter.chain($trigger_targets.components()); + )* + iter + } + + fn entities(&self) -> impl Iterator + Clone + '_ { + let iter = [].into_iter(); + let ($($trigger_targets,)*) = self; + $( + let iter = iter.chain($trigger_targets.entities()); + )* + iter + } + } + } +} + +all_tuples!( + #[doc(fake_variadic)] + impl_trigger_targets_tuples, + 0, + 15, + T +); diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 9c63cb5a74..0c5b29f715 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -4,7 +4,6 @@ use crate::world::World; use alloc::{format, string::String, vec, vec::Vec}; use core::{fmt, fmt::Debug, marker::PhantomData}; use derive_more::From; -use disqualified::ShortName; use fixedbitset::FixedBitSet; use thiserror::Error; @@ -999,12 +998,11 @@ impl AccessConflicts { .map(|index| { format!( "{}", - ShortName( - &world - .components - .get_name(ComponentId::get_sparse_set_index(index)) - .unwrap() - ) + world + .components + .get_name(ComponentId::get_sparse_set_index(index)) + .unwrap() + .shortname() ) }) .collect::>() diff --git a/crates/bevy_ecs/src/query/error.rs b/crates/bevy_ecs/src/query/error.rs index 6d0b149b86..fd431f4be1 100644 --- a/crates/bevy_ecs/src/query/error.rs +++ b/crates/bevy_ecs/src/query/error.rs @@ -1,3 +1,4 @@ +use bevy_utils::prelude::DebugName; use thiserror::Error; use crate::{ @@ -54,10 +55,10 @@ impl core::fmt::Display for QueryEntityError { pub enum QuerySingleError { /// No entity fits the query. #[error("No entities fit the query {0}")] - NoEntities(&'static str), + NoEntities(DebugName), /// Multiple entities fit the query. #[error("Multiple entities fit the query {0}")] - MultipleEntities(&'static str), + MultipleEntities(DebugName), } #[cfg(test)] diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 3c1ff5262c..2564223972 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -12,6 +12,7 @@ use crate::{ }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; @@ -47,6 +48,8 @@ use variadics_please::all_tuples; /// - **[`Ref`].** /// Similar to change detection filters but it is used as a query fetch parameter. /// It exposes methods to check for changes to the wrapped component. +/// - **[`Mut`].** +/// Mutable component access, with change detection data. /// - **[`Has`].** /// Returns a bool indicating whether the entity has the specified component. /// @@ -161,7 +164,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryItem` is only available when accessing the query with mutable methods. -/// impl<'w> HealthQueryItem<'w> { +/// impl<'w, 's> HealthQueryItem<'w, 's> { /// fn damage(&mut self, value: f32) { /// self.health.0 -= value; /// } @@ -172,7 +175,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryReadOnlyItem` is only available when accessing the query with immutable methods. -/// impl<'w> HealthQueryReadOnlyItem<'w> { +/// impl<'w, 's> HealthQueryReadOnlyItem<'w, 's> { /// fn total(&self) -> f32 { /// self.health.0 + self.buff.map_or(0.0, |Buff(buff)| *buff) /// } @@ -288,10 +291,12 @@ pub unsafe trait QueryData: WorldQuery { /// The item returned by this [`WorldQuery`] /// This will be the data retrieved by the query, /// and is visible to the end user when calling e.g. `Query::get`. - type Item<'a>; + type Item<'w, 's>; /// This function manually implements subtyping for the query items. - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort>; + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's>; /// Offers additional access above what we requested in `update_component_access`. /// Implementations may add additional access that is a subset of `available_access` @@ -320,11 +325,12 @@ pub unsafe trait QueryData: WorldQuery { /// - Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. /// - There must not be simultaneous conflicting component access registered in `update_component_access`. - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w>; + ) -> Self::Item<'w, 's>; } /// A [`QueryData`] that is read only. @@ -335,12 +341,23 @@ pub unsafe trait QueryData: WorldQuery { pub unsafe trait ReadOnlyQueryData: QueryData {} /// The item type returned when a [`WorldQuery`] is iterated over -pub type QueryItem<'w, Q> = ::Item<'w>; +pub type QueryItem<'w, 's, Q> = ::Item<'w, 's>; /// The read-only variant of the item type returned when a [`QueryData`] is iterated over immutably -pub type ROQueryItem<'w, D> = QueryItem<'w, ::ReadOnly>; +pub type ROQueryItem<'w, 's, D> = QueryItem<'w, 's, ::ReadOnly>; + +/// A [`QueryData`] that does not borrow from its [`QueryState`](crate::query::QueryState). +/// +/// This is implemented by most `QueryData` types. +/// The main exceptions are [`FilteredEntityRef`], [`FilteredEntityMut`], [`EntityRefExcept`], and [`EntityMutExcept`], +/// which borrow an access list from their query state. +/// Consider using a full [`EntityRef`] or [`EntityMut`] if you would need those. +pub trait ReleaseStateQueryData: QueryData { + /// Releases the borrow from the query state by converting an item to have a `'static` state lifetime. + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static>; +} /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Entity { type Fetch<'w> = (); @@ -348,9 +365,9 @@ unsafe impl WorldQuery for Entity { fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -359,16 +376,20 @@ unsafe impl WorldQuery for Entity { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -392,18 +413,21 @@ unsafe impl QueryData for Entity { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { entity } } @@ -411,8 +435,14 @@ unsafe impl QueryData for Entity { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for Entity {} +impl ReleaseStateQueryData for Entity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for EntityLocation { type Fetch<'w> = &'w Entities; @@ -422,9 +452,9 @@ unsafe impl WorldQuery for EntityLocation { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -436,16 +466,20 @@ unsafe impl WorldQuery for EntityLocation { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -468,18 +502,21 @@ unsafe impl WorldQuery for EntityLocation { unsafe impl QueryData for EntityLocation { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityLocation; + type Item<'w, 's> = EntityLocation; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world unsafe { fetch.get(entity).debug_checked_unwrap() } } @@ -488,6 +525,12 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} +impl ReleaseStateQueryData for EntityLocation { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at. /// /// To evaluate whether the spawn happened since the last time the system ran, the system @@ -518,7 +561,7 @@ unsafe impl ReadOnlyQueryData for EntityLocation {} /// match spawn_details.spawned_by().into_option() { /// Some(location) => println!(" by {:?}", location), /// None => println!() -/// } +/// } /// } /// } /// @@ -568,9 +611,9 @@ unsafe impl WorldQuery for SpawnDetails { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -584,16 +627,20 @@ unsafe impl WorldQuery for SpawnDetails { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -618,18 +665,21 @@ unsafe impl WorldQuery for SpawnDetails { unsafe impl QueryData for SpawnDetails { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Self; + type Item<'w, 's> = Self; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: only living entities are queried let (spawned_by, spawned_at) = unsafe { fetch @@ -648,6 +698,12 @@ unsafe impl QueryData for SpawnDetails { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for SpawnDetails {} +impl ReleaseStateQueryData for SpawnDetails { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity /// ([`EntityRef`], [`EntityMut`], etc.) #[derive(Copy, Clone)] @@ -660,7 +716,7 @@ pub struct EntityFetch<'w> { /// SAFETY: /// `fetch` accesses all components in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` set read access for all components and panic when appropriate. +/// This is sound because `update_component_access` sets read access for all components and panic when appropriate. /// Filters are unchanged. unsafe impl<'a> WorldQuery for EntityRef<'a> { type Fetch<'w> = EntityFetch<'w>; @@ -670,9 +726,9 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -686,16 +742,20 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -724,18 +784,21 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { unsafe impl<'a> QueryData for EntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRef<'w>; + type Item<'w, 's> = EntityRef<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -751,6 +814,12 @@ unsafe impl<'a> QueryData for EntityRef<'a> { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityRef<'_> {} +impl ReleaseStateQueryData for EntityRef<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for EntityMut<'a> { type Fetch<'w> = EntityFetch<'w>; @@ -760,9 +829,9 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -776,16 +845,20 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -814,18 +887,21 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { unsafe impl<'a> QueryData for EntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRef<'a>; - type Item<'w> = EntityMut<'w>; + type Item<'w, 's> = EntityMut<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -838,6 +914,12 @@ unsafe impl<'a> QueryData for EntityMut<'a> { } } +impl ReleaseStateQueryData for EntityMut<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { type Fetch<'w> = (EntityFetch<'w>, Access); @@ -849,9 +931,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { const IS_DENSE: bool = false; - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -868,9 +950,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -878,7 +960,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -913,9 +995,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { unsafe impl<'a> QueryData for FilteredEntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = FilteredEntityRef<'w>; + type Item<'w, 's> = FilteredEntityRef<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } @@ -939,11 +1023,12 @@ unsafe impl<'a> QueryData for FilteredEntityRef<'a> { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -970,9 +1055,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { const IS_DENSE: bool = false; - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -989,9 +1074,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -999,7 +1084,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -1034,9 +1119,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { unsafe impl<'a> QueryData for FilteredEntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = FilteredEntityRef<'a>; - type Item<'w> = FilteredEntityMut<'w>; + type Item<'w, 's> = FilteredEntityMut<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } @@ -1058,11 +1145,12 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -1089,9 +1177,9 @@ where fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1104,15 +1192,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _: &mut Self::Fetch<'w>, - _: &Self::State, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1128,7 +1216,7 @@ where assert!( access.is_compatible(&my_access), "`EntityRefExcept<{}>` conflicts with a previous access in this query.", - core::any::type_name::(), + DebugName::type_name::(), ); access.extend(&my_access); } @@ -1159,17 +1247,20 @@ where { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRefExcept<'w, B>; + type Item<'w, 's> = EntityRefExcept<'w, B>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1196,9 +1287,9 @@ where fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1211,15 +1302,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _: &mut Self::Fetch<'w>, - _: &Self::State, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1235,7 +1326,7 @@ where assert!( access.is_compatible(&my_access), "`EntityMutExcept<{}>` conflicts with a previous access in this query.", - core::any::type_name::() + DebugName::type_name::() ); access.extend(&my_access); } @@ -1267,17 +1358,20 @@ where { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRefExcept<'a, B>; - type Item<'w> = EntityMutExcept<'w, B>; + type Item<'w, 's> = EntityMutExcept<'w, B>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1287,7 +1381,7 @@ where } /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for &Archetype { type Fetch<'w> = (&'w Entities, &'w Archetypes); @@ -1297,9 +1391,9 @@ unsafe impl WorldQuery for &Archetype { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -1311,16 +1405,20 @@ unsafe impl WorldQuery for &Archetype { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -1343,18 +1441,21 @@ unsafe impl WorldQuery for &Archetype { unsafe impl QueryData for &Archetype { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w Archetype; + type Item<'w, 's> = &'w Archetype; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let (entities, archetypes) = *fetch; // SAFETY: `fetch` must be called with an entity that exists in the world let location = unsafe { entities.get(entity).debug_checked_unwrap() }; @@ -1366,6 +1467,12 @@ unsafe impl QueryData for &Archetype { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &Archetype {} +impl ReleaseStateQueryData for &Archetype { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `& T`. pub struct ReadFetch<'w, T: Component> { components: StorageSwitch< @@ -1382,11 +1489,12 @@ impl Clone for ReadFetch<'_, T> { *self } } + impl Copy for ReadFetch<'_, T> {} /// SAFETY: /// `fetch` accesses a single component in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` add read access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds read access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl WorldQuery for &T { @@ -1398,7 +1506,7 @@ unsafe impl WorldQuery for &T { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, _last_run: Tick, @@ -1409,7 +1517,7 @@ unsafe impl WorldQuery for &T { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components in this function, we just get a shared // reference to the sparse set, which is used to access the components in `Self::fetch`. unsafe { world.storages().sparse_sets.get(component_id) } @@ -1463,7 +1571,7 @@ unsafe impl WorldQuery for &T { assert!( !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_read(component_id); } @@ -1488,24 +1596,27 @@ unsafe impl WorldQuery for &T { unsafe impl QueryData for &T { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w T; + type Item<'w, 's> = &'w T; - fn shrink<'wlong: 'wshort, 'wshort>(item: &'wlong T) -> &'wshort T { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: Caller ensures `table_row` is in range. - let item = unsafe { table.get(table_row.as_usize()) }; + let item = unsafe { table.get(table_row.index()) }; item.deref() }, |sparse_set| { @@ -1525,6 +1636,12 @@ unsafe impl QueryData for &T { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &T {} +impl ReleaseStateQueryData for &T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + #[doc(hidden)] pub struct RefFetch<'w, T: Component> { components: StorageSwitch< @@ -1549,11 +1666,12 @@ impl Clone for RefFetch<'_, T> { *self } } + impl Copy for RefFetch<'_, T> {} /// SAFETY: /// `fetch` accesses a single component in a readonly way. -/// This is sound because `update_component_access` and `update_archetype_component_access` add read access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds read access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { @@ -1565,7 +1683,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1576,7 +1694,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components in this function, we just get a shared // reference to the sparse set, which is used to access the components in `Self::fetch`. unsafe { world.storages().sparse_sets.get(component_id) } @@ -1617,11 +1735,15 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { ) { let column = table.get_column(component_id).debug_checked_unwrap(); let table_data = Some(( - column.get_data_slice(table.entity_count()).into(), - column.get_added_ticks_slice(table.entity_count()).into(), - column.get_changed_ticks_slice(table.entity_count()).into(), + column.get_data_slice(table.entity_count() as usize).into(), column - .get_changed_by_slice(table.entity_count()) + .get_added_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_by_slice(table.entity_count() as usize) .map(Into::into), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1635,7 +1757,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { assert!( !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_read(component_id); } @@ -1660,18 +1782,21 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Ref<'w, T>; + type Item<'w, 's> = Ref<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Ref<'wlong, T>) -> Ref<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1679,13 +1804,13 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let component = unsafe { table_components.get(table_row.as_usize()) }; + let component = unsafe { table_components.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let added = unsafe { added_ticks.get(table_row.as_usize()) }; + let added = unsafe { added_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; + let changed = unsafe { changed_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let caller = callers.map(|callers| unsafe { callers.get(table_row.as_usize()) }); + let caller = callers.map(|callers| unsafe { callers.get(table_row.index()) }); Ref { value: component.deref(), @@ -1720,6 +1845,12 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { /// SAFETY: access is read only unsafe impl<'__w, T: Component> ReadOnlyQueryData for Ref<'__w, T> {} +impl ReleaseStateQueryData for Ref<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `&mut T`. pub struct WriteFetch<'w, T: Component> { components: StorageSwitch< @@ -1744,11 +1875,12 @@ impl Clone for WriteFetch<'_, T> { *self } } + impl Copy for WriteFetch<'_, T> {} /// SAFETY: /// `fetch` accesses a single component mutably. -/// This is sound because `update_component_access` and `update_archetype_component_access` add write access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds write access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { @@ -1760,7 +1892,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1771,7 +1903,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components in this function, we just get a shared // reference to the sparse set, which is used to access the components in `Self::fetch`. unsafe { world.storages().sparse_sets.get(component_id) } @@ -1812,11 +1944,15 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { ) { let column = table.get_column(component_id).debug_checked_unwrap(); let table_data = Some(( - column.get_data_slice(table.entity_count()).into(), - column.get_added_ticks_slice(table.entity_count()).into(), - column.get_changed_ticks_slice(table.entity_count()).into(), + column.get_data_slice(table.entity_count() as usize).into(), column - .get_changed_by_slice(table.entity_count()) + .get_added_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_ticks_slice(table.entity_count() as usize) + .into(), + column + .get_changed_by_slice(table.entity_count() as usize) .map(Into::into), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1830,7 +1966,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { assert!( !access.access().has_component_read(component_id), "&mut {} conflicts with a previous access in this query. Mutable component access must be unique.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_write(component_id); } @@ -1855,18 +1991,21 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe impl<'__w, T: Component> QueryData for &'__w mut T { const IS_READ_ONLY: bool = false; type ReadOnly = &'__w T; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1874,13 +2013,13 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let component = unsafe { table_components.get(table_row.as_usize()) }; + let component = unsafe { table_components.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let added = unsafe { added_ticks.get(table_row.as_usize()) }; + let added = unsafe { added_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; + let changed = unsafe { changed_ticks.get(table_row.index()) }; // SAFETY: The caller ensures `table_row` is in range. - let caller = callers.map(|callers| unsafe { callers.get(table_row.as_usize()) }); + let caller = callers.map(|callers| unsafe { callers.get(table_row.index()) }); Mut { value: component.deref_mut(), @@ -1912,13 +2051,19 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T } } +impl> ReleaseStateQueryData for &mut T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// When `Mut` is used in a query, it will be converted to `Ref` when transformed into its read-only form, providing access to change detection methods. /// /// By contrast `&mut T` will result in a `Mut` item in mutable form to record mutations, but result in a bare `&T` in read-only form. /// /// SAFETY: /// `fetch` accesses a single component mutably. -/// This is sound because `update_component_access` and `update_archetype_component_access` add write access for that component and panic when appropriate. +/// This is sound because `update_component_access` adds write access for that component and panic when appropriate. /// `update_component_access` adds a `With` filter for a component. /// This is sound because `matches_component_set` returns whether the set contains that component. unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { @@ -1931,7 +2076,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { #[inline] // Forwarded to `&mut T` - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, state: &ComponentId, last_run: Tick, @@ -1970,7 +2115,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { assert!( !access.access().has_component_read(component_id), "Mut<{}> conflicts with a previous access in this query. Mutable component access mut be unique.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_write(component_id); } @@ -1998,23 +2143,32 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> { const IS_READ_ONLY: bool = false; type ReadOnly = Ref<'__w, T>; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; // Forwarded to `&mut T` - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { <&mut T as QueryData>::shrink(item) } #[inline(always)] // Forwarded to `&mut T` - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, // Rust complains about lifetime bounds not matching the trait if I directly use `WriteFetch<'w, T>` right here. // But it complains nowhere else in the entire trait implementation. fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Mut<'w, T> { - <&mut T as QueryData>::fetch(fetch, entity, table_row) + ) -> Self::Item<'w, 's> { + <&mut T as QueryData>::fetch(state, fetch, entity, table_row) + } +} + +impl> ReleaseStateQueryData for Mut<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item } } @@ -2035,7 +2189,7 @@ impl Clone for OptionFetch<'_, T> { /// SAFETY: /// `fetch` might access any components that `T` accesses. -/// This is sound because `update_component_access` and `update_archetype_component_access` add the same accesses as `T`. +/// This is sound because `update_component_access` adds the same accesses as `T`. /// Filters are unchanged. unsafe impl WorldQuery for Option { type Fetch<'w> = OptionFetch<'w, T>; @@ -2049,9 +2203,9 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &T::State, + state: &'s T::State, last_run: Tick, this_run: Tick, ) -> OptionFetch<'w, T> { @@ -2065,9 +2219,9 @@ unsafe impl WorldQuery for Option { const IS_DENSE: bool = T::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut OptionFetch<'w, T>, - state: &T::State, + state: &'s T::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -2081,7 +2235,11 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn set_table<'w>(fetch: &mut OptionFetch<'w, T>, state: &T::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut OptionFetch<'w, T>, + state: &'s T::State, + table: &'w Table, + ) { fetch.matches = T::matches_component_set(state, &|id| table.has_column(id)); if fetch.matches { // SAFETY: The invariants are upheld by the caller. @@ -2126,28 +2284,37 @@ unsafe impl WorldQuery for Option { unsafe impl QueryData for Option { const IS_READ_ONLY: bool = T::IS_READ_ONLY; type ReadOnly = Option; - type Item<'w> = Option>; + type Item<'w, 's> = Option>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item.map(T::shrink) } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch .matches // SAFETY: The invariants are upheld by the caller. - .then(|| unsafe { T::fetch(&mut fetch.fetch, entity, table_row) }) + .then(|| unsafe { T::fetch(state, &mut fetch.fetch, entity, table_row) }) } } /// SAFETY: [`OptionFetch`] is read only because `T` is read only unsafe impl ReadOnlyQueryData for Option {} +impl ReleaseStateQueryData for Option { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item.map(T::release_state) + } +} + /// Returns a bool that describes if an entity has the component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if you want to know whether or not entities @@ -2215,12 +2382,12 @@ pub struct Has(PhantomData); impl core::fmt::Debug for Has { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - write!(f, "Has<{}>", core::any::type_name::()) + write!(f, "Has<{}>", DebugName::type_name::()) } } /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for Has { type Fetch<'w> = bool; @@ -2231,9 +2398,9 @@ unsafe impl WorldQuery for Has { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -2248,9 +2415,9 @@ unsafe impl WorldQuery for Has { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, _table: &Table, ) { @@ -2258,7 +2425,11 @@ unsafe impl WorldQuery for Has { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &'s Self::State, + table: &'w Table, + ) { *fetch = table.has_column(*state); } @@ -2290,18 +2461,21 @@ unsafe impl WorldQuery for Has { unsafe impl QueryData for Has { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = bool; + type Item<'w, 's> = bool; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { *fetch } } @@ -2309,6 +2483,12 @@ unsafe impl QueryData for Has { /// SAFETY: [`Has`] is read only unsafe impl ReadOnlyQueryData for Has {} +impl ReleaseStateQueryData for Has { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// /// `Query>` is equivalent to `Query<(Option<&A>, Option<&B>, Option<&mut C>), Or<(With, With, With)>>`. @@ -2317,7 +2497,7 @@ unsafe impl ReadOnlyQueryData for Has {} pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $item: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -2339,9 +2519,9 @@ macro_rules! impl_tuple_query_data { unsafe impl<$($name: QueryData),*> QueryData for ($($name,)*) { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = ($($name::ReadOnly,)*); - type Item<'w> = ($($name::Item<'w>,)*); + type Item<'w, 's> = ($($name::Item<'w, 's>,)*); - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { let ($($name,)*) = item; ($( $name::shrink($name), @@ -2359,26 +2539,40 @@ macro_rules! impl_tuple_query_data { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - ($(unsafe { $name::fetch($name, entity, table_row) },)*) + ($(unsafe { $name::fetch($state, $name, entity, table_row) },)*) } } - $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {} + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for ($($name,)*) { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($name::release_state($item),)*) + } + } }; } macro_rules! impl_anytuple_fetch { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident, $item: ident)),*) => { $(#[$meta])* #[expect( clippy::allow_attributes, @@ -2398,7 +2592,7 @@ macro_rules! impl_anytuple_fetch { )] /// SAFETY: /// `fetch` accesses are a subset of the subqueries' accesses - /// This is sound because `update_component_access` and `update_archetype_component_access` adds accesses according to the implementations of all the subqueries. + /// This is sound because `update_component_access` adds accesses according to the implementations of all the subqueries. /// `update_component_access` replaces the filters with a disjunction where every element is a conjunction of the previous filters and the filters of one of the subqueries. /// This is sound because `matches_component_set` returns a disjunction of the results of the subqueries' implementations. unsafe impl<$($name: WorldQuery),*> WorldQuery for AnyOf<($($name,)*)> { @@ -2413,7 +2607,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn init_fetch<'w>(_world: UnsafeWorldCell<'w>, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(_world: UnsafeWorldCell<'w>, state: &'s Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(( unsafe { $name::init_fetch(_world, $name, _last_run, _this_run) }, false),)*) @@ -2422,9 +2616,9 @@ macro_rules! impl_anytuple_fetch { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table ) { @@ -2440,7 +2634,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table) { let ($($name,)*) = _fetch; let ($($state,)*) = _state; $( @@ -2512,9 +2706,9 @@ macro_rules! impl_anytuple_fetch { unsafe impl<$($name: QueryData),*> QueryData for AnyOf<($($name,)*)> { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = AnyOf<($($name::ReadOnly,)*)>; - type Item<'w> = ($(Option<$name::Item<'w>>,)*); + type Item<'w, 's> = ($(Option<$name::Item<'w, 's>>,)*); - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { let ($($name,)*) = item; ($( $name.map($name::shrink), @@ -2522,15 +2716,17 @@ macro_rules! impl_anytuple_fetch { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let ($($name,)*) = _fetch; + let ($($state,)*) = _state; ($( // SAFETY: The invariants are required to be upheld by the caller. - $name.1.then(|| unsafe { $name::fetch(&mut $name.0, _entity, _table_row) }), + $name.1.then(|| unsafe { $name::fetch($state, &mut $name.0, _entity, _table_row) }), )*) } } @@ -2538,6 +2734,20 @@ macro_rules! impl_anytuple_fetch { $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for AnyOf<($($name,)*)> {} + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for AnyOf<($($name,)*)> { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($item.map(|$item| $name::release_state($item)),)*) + } + } }; } @@ -2547,7 +2757,8 @@ all_tuples!( 0, 15, F, - S + i, + s ); all_tuples!( #[doc(fake_variadic)] @@ -2555,7 +2766,8 @@ all_tuples!( 0, 15, F, - S + S, + i ); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` @@ -2564,13 +2776,14 @@ all_tuples!( pub(crate) struct NopWorldQuery(PhantomData); /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for NopWorldQuery { type Fetch<'w> = (); type State = D::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + } #[inline(always)] unsafe fn init_fetch( @@ -2617,36 +2830,44 @@ unsafe impl WorldQuery for NopWorldQuery { unsafe impl QueryData for NopWorldQuery { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `NopFetch` never accesses any data unsafe impl ReadOnlyQueryData for NopWorldQuery {} +impl ReleaseStateQueryData for NopWorldQuery { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// SAFETY: -/// `update_component_access` and `update_archetype_component_access` do nothing. +/// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for PhantomData { - type Fetch<'a> = (); + type Fetch<'w> = (); type State = (); fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -2656,15 +2877,19 @@ unsafe impl WorldQuery for PhantomData { // are stored in a Table (vacuous truth). const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -2687,21 +2912,29 @@ unsafe impl WorldQuery for PhantomData { unsafe impl QueryData for PhantomData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'a> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `PhantomData` never accesses any world data. unsafe impl ReadOnlyQueryData for PhantomData {} +impl ReleaseStateQueryData for PhantomData { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// A compile-time checked union of two different types that differs based on the /// [`StorageType`] of a given component. pub(super) union StorageSwitch { @@ -2818,6 +3051,124 @@ mod tests { assert_is_system(ignored_system); } + #[test] + fn derive_release_state() { + struct NonReleaseQueryData; + + /// SAFETY: + /// `update_component_access` and `update_archetype_component_access` do nothing. + /// This is sound because `fetch` does not access components. + unsafe impl WorldQuery for NonReleaseQueryData { + type Fetch<'w> = (); + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + _: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { + } + + unsafe fn init_fetch<'w, 's>( + _world: UnsafeWorldCell<'w>, + _state: &'s Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { + } + + fn update_component_access( + _state: &Self::State, + _access: &mut FilteredAccess, + ) { + } + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } + } + + /// SAFETY: `Self` is the same as `Self::ReadOnly` + unsafe impl QueryData for NonReleaseQueryData { + type ReadOnly = Self; + const IS_READ_ONLY: bool = true; + + type Item<'w, 's> = (); + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } + + #[inline(always)] + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, + _entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w, 's> { + } + } + + /// SAFETY: access is read only + unsafe impl ReadOnlyQueryData for NonReleaseQueryData {} + + #[derive(QueryData)] + pub struct DerivedNonReleaseRead { + non_release: NonReleaseQueryData, + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedNonReleaseMutable { + non_release: NonReleaseQueryData, + a: &'static mut A, + } + + #[derive(QueryData)] + pub struct DerivedReleaseRead { + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedReleaseMutable { + a: &'static mut A, + } + + fn assert_is_release_state() {} + + assert_is_release_state::(); + assert_is_release_state::(); + } + // Ensures that each field of a `WorldQuery` struct's read-only variant // has the same visibility as its corresponding mutable field. #[test] diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 38c7cfcb32..f9f4861b79 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -7,6 +7,7 @@ use crate::{ world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, marker::PhantomData}; use variadics_please::all_tuples; @@ -103,6 +104,7 @@ pub unsafe trait QueryFilter: WorldQuery { /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -204,6 +206,7 @@ unsafe impl QueryFilter for With { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, @@ -304,6 +307,7 @@ unsafe impl QueryFilter for Without { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, @@ -400,7 +404,7 @@ macro_rules! impl_or_query_filter { const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($filter,)*) = state; ($(OrFetch { // SAFETY: The invariants are upheld by the caller. @@ -410,7 +414,7 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($filter,)*) = fetch; let ($($state,)*) = state; $( @@ -423,9 +427,9 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: & Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -495,20 +499,22 @@ macro_rules! impl_or_query_filter { #[inline(always)] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($filter,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - false $(|| ($filter.matches && unsafe { $filter::filter_fetch(&mut $filter.fetch, entity, table_row) }))* + false $(|| ($filter.matches && unsafe { $filter::filter_fetch($state, &mut $filter.fetch, entity, table_row) }))* } } }; } macro_rules! impl_tuple_query_filter { - ($(#[$meta:meta])* $($name: ident),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -528,13 +534,15 @@ macro_rules! impl_tuple_query_filter { #[inline(always)] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - true $(&& unsafe { $name::filter_fetch($name, entity, table_row) })* + true $(&& unsafe { $name::filter_fetch($state, $name, entity, table_row) })* } } @@ -546,7 +554,8 @@ all_tuples!( impl_tuple_query_filter, 0, 15, - F + F, + S ); all_tuples!( #[doc(fake_variadic)] @@ -609,7 +618,12 @@ unsafe impl QueryFilter for Allows { const IS_ARCHETYPAL: bool = true; #[inline(always)] - unsafe fn filter_fetch(_: &mut Self::Fetch<'_>, _: Entity, _: TableRow) -> bool { + unsafe fn filter_fetch( + _: &Self::State, + _: &mut Self::Fetch<'_>, + _: Entity, + _: TableRow, + ) -> bool { true } } @@ -718,9 +732,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -729,7 +743,7 @@ unsafe impl WorldQuery for Added { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components' ticks in this function, we just get a shared // reference to the sparse set, which is used to access the components' ticks in `Self::fetch`. unsafe { world.storages().sparse_sets.get(id) } @@ -748,9 +762,9 @@ unsafe impl WorldQuery for Added { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -763,9 +777,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -781,7 +795,7 @@ unsafe impl WorldQuery for Added { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { if access.access().has_component_write(id) { - panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",core::any::type_name::()); + panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::()); } access.add_component_read(id); } @@ -807,6 +821,7 @@ unsafe impl QueryFilter for Added { const IS_ARCHETYPAL: bool = false; #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -817,7 +832,7 @@ unsafe impl QueryFilter for Added { // SAFETY: set_table was previously called let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let tick = unsafe { table.get(table_row.as_usize()) }; + let tick = unsafe { table.get(table_row.index()) }; tick.deref().is_newer_than(fetch.last_run, fetch.this_run) }, @@ -944,9 +959,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -955,7 +970,7 @@ unsafe impl WorldQuery for Changed { || None, || { // SAFETY: The underlying type associated with `component_id` is `T`, - // which we are allowed to access since we registered it in `update_archetype_component_access`. + // which we are allowed to access since we registered it in `update_component_access`. // Note that we do not actually access any components' ticks in this function, we just get a shared // reference to the sparse set, which is used to access the components' ticks in `Self::fetch`. unsafe { world.storages().sparse_sets.get(id) } @@ -974,9 +989,9 @@ unsafe impl WorldQuery for Changed { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -989,9 +1004,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -1007,7 +1022,7 @@ unsafe impl WorldQuery for Changed { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { if access.access().has_component_write(id) { - panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",core::any::type_name::()); + panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::()); } access.add_component_read(id); } @@ -1034,6 +1049,7 @@ unsafe impl QueryFilter for Changed { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -1044,7 +1060,7 @@ unsafe impl QueryFilter for Changed { // SAFETY: set_table was previously called let table = unsafe { table.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. - let tick = unsafe { table.get(table_row.as_usize()) }; + let tick = unsafe { table.get(table_row.index()) }; tick.deref().is_newer_than(fetch.last_run, fetch.this_run) }, @@ -1141,9 +1157,9 @@ unsafe impl WorldQuery for Spawned { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &(), + _state: &'s (), last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1157,16 +1173,16 @@ unsafe impl WorldQuery for Spawned { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &(), + _state: &'s (), _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &(), _table: &'w Table) {} + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s (), _table: &'w Table) {} #[inline] fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} @@ -1188,6 +1204,7 @@ unsafe impl QueryFilter for Spawned { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, _table_row: TableRow, @@ -1223,6 +1240,7 @@ unsafe impl QueryFilter for Spawned { pub trait ArchetypeFilter: QueryFilter {} impl ArchetypeFilter for With {} + impl ArchetypeFilter for Without {} macro_rules! impl_archetype_filter_tuple { diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index fc89843493..eb49204434 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -19,6 +19,7 @@ use core::{ mem::MaybeUninit, ops::Range, }; +use nonmax::NonMaxU32; /// An [`Iterator`] over query results of a [`Query`](crate::system::Query). /// @@ -136,10 +137,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, storage: StorageId, - range: Option>, + range: Option>, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if self.cursor.is_dense { // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. @@ -199,18 +200,14 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, table: &'w Table, - rows: Range, + rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if table.is_empty() { return accum; } - debug_assert!( - rows.end <= u32::MAX as usize, - "TableRow is only valid up to u32::MAX" - ); D::set_table(&mut self.cursor.fetch, &self.query_state.fetch_state, table); F::set_table( @@ -222,19 +219,32 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let entities = table.entities(); for row in rows { // SAFETY: Caller assures `row` in range of the current archetype. - let entity = unsafe { entities.get_unchecked(row) }; - let row = TableRow::from_usize(row); + let entity = unsafe { entities.get_unchecked(row as usize) }; + // SAFETY: This is from an exclusive range, so it can't be max. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(row)) }; // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let fetched = unsafe { !F::filter_fetch(&mut self.cursor.filter, *entity, row) }; + let fetched = unsafe { + !F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + *entity, + row, + ) + }; if fetched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, *entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + *entity, + row, + ); accum = func(accum, item); } @@ -254,10 +264,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, archetype: &'w Archetype, - indices: Range, + indices: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -279,12 +289,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let entities = archetype.entities(); for index in indices { // SAFETY: Caller assures `index` in range of the current archetype. - let archetype_entity = unsafe { entities.get_unchecked(index) }; + let archetype_entity = unsafe { entities.get_unchecked(index as usize) }; // SAFETY: set_archetype was called prior. // Caller assures `index` in range of the current archetype. let fetched = unsafe { !F::filter_fetch( + &self.query_state.filter_state, &mut self.cursor.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -298,6 +309,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let item = unsafe { D::fetch( + &self.query_state.fetch_state, &mut self.cursor.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -315,7 +327,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// # Safety /// - all `indices` must be in `[0, archetype.len())`. /// - `archetype` must match D and F - /// - `archetype` must have the same length with it's table. + /// - `archetype` must have the same length as its table. /// - The query iteration must not be dense (i.e. `self.query_state.is_dense` must be false). #[inline] pub(super) unsafe fn fold_over_dense_archetype_range( @@ -323,22 +335,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { mut accum: B, func: &mut Func, archetype: &'w Archetype, - rows: Range, + rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; } - debug_assert!( - rows.end <= u32::MAX as usize, - "TableRow is only valid up to u32::MAX" - ); let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); debug_assert!( archetype.len() == table.entity_count(), - "archetype and it's table must have the same length. " + "archetype and its table must have the same length. " ); D::set_archetype( @@ -356,19 +364,32 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let entities = table.entities(); for row in rows { // SAFETY: Caller assures `row` in range of the current archetype. - let entity = unsafe { *entities.get_unchecked(row) }; - let row = TableRow::from_usize(row); + let entity = unsafe { *entities.get_unchecked(row as usize) }; + // SAFETY: This is from an exclusive range, so it can't be max. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(row)) }; // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let filter_matched = unsafe { F::filter_fetch(&mut self.cursor.filter, entity, row) }; + let filter_matched = unsafe { + F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + entity, + row, + ) + }; if !filter_matched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + entity, + row, + ); accum = func(accum, item); } @@ -497,7 +518,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -554,7 +575,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -610,7 +631,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -642,7 +663,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -734,7 +755,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -767,7 +788,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -802,7 +823,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -832,7 +853,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedIter< 'w, 's, @@ -861,7 +882,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { .map(|(key, entity)| (key, NeutralOrd(entity))) .collect(); f(&mut keyed_query); - let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity.0); + let entity_iter = keyed_query + .into_iter() + .map(|(.., entity)| entity.0) + .collect::>() + .into_iter(); // SAFETY: // `self.world` has permission to access the required components. // Each lens query item is dropped before the respective actual query item is accessed. @@ -878,7 +903,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -895,7 +920,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> let max_size = self.cursor.max_remaining(self.tables, self.archetypes); let archetype_query = F::IS_ARCHETYPAL; let min_size = if archetype_query { max_size } else { 0 }; - (min_size, Some(max_size)) + (min_size as usize, Some(max_size as usize)) } #[inline] @@ -1015,7 +1040,7 @@ where /// # Safety /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1044,7 +1069,14 @@ where // 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) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } } @@ -1053,7 +1085,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Iterator where I: Iterator, { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1175,7 +1207,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> fetch: &mut D::Fetch<'w>, filter: &mut F::Fetch<'w>, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { for entity_borrow in entity_iter { let entity = entity_borrow.entity(); let Some(location) = entities.get(entity) else { @@ -1205,11 +1237,20 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // 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) } { + if unsafe { + F::filter_fetch( + &query_state.filter_state, + 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) }); + return Some(unsafe { + D::fetch(&query_state.fetch_state, fetch, entity, location.table_row) + }); } } None @@ -1217,7 +1258,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1341,7 +1382,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -1399,7 +1440,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -1456,7 +1497,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1487,7 +1528,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1581,7 +1622,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1613,7 +1654,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1647,7 +1688,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1676,7 +1717,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedManyIter< 'w, 's, @@ -1726,7 +1767,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator Option> { + pub fn fetch_next_back(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1750,7 +1791,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator> Iterator for QueryManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1866,7 +1907,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> Iterator for QueryManyUniqueIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1959,7 +2000,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// 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> { + unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1988,12 +2029,19 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // 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) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { let entity = self.entity_iter.next()?; // SAFETY: @@ -2012,7 +2060,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator { /// Get next result from the query #[inline(always)] - pub fn fetch_next_back(&mut self) -> Option> { + pub fn fetch_next_back(&mut self) -> Option> { let entity = self.entity_iter.next_back()?; // SAFETY: @@ -2029,7 +2077,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator> Iterator for QuerySortedManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -2190,7 +2238,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// . /// It is always safe for shared access. #[inline] - unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w>; K]> { + unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w, 's>; K]> { // PERF: can speed up the following code using `cursor.remaining()` instead of `next_item.is_none()` // when D::IS_ARCHETYPAL && F::IS_ARCHETYPAL // @@ -2216,11 +2264,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< } } - let mut values = MaybeUninit::<[D::Item<'w>; K]>::uninit(); + let mut values = MaybeUninit::<[D::Item<'w, 's>; K]>::uninit(); - let ptr = values.as_mut_ptr().cast::>(); + let ptr = values.as_mut_ptr().cast::>(); for (offset, cursor) in self.cursors.iter_mut().enumerate() { - ptr.add(offset).write(cursor.peek_last().unwrap()); + ptr.add(offset) + .write(cursor.peek_last(self.query_state).unwrap()); } Some(values.assume_init()) @@ -2228,7 +2277,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// Get next combination of queried components #[inline] - pub fn fetch_next(&mut self) -> Option<[D::Item<'_>; K]> { + pub fn fetch_next(&mut self) -> Option<[D::Item<'_, 's>; K]> { // SAFETY: 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. @@ -2245,7 +2294,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> Iterator for QueryCombinationIter<'w, 's, D, F, K> { - type Item = [D::Item<'w>; K]; + type Item = [D::Item<'w, 's>; K]; #[inline] fn next(&mut self) -> Option { @@ -2275,7 +2324,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> Iterator .enumerate() .try_fold(0, |acc, (i, cursor)| { let n = cursor.max_remaining(self.tables, self.archetypes); - Some(acc + choose(n, K - i)?) + Some(acc + choose(n as usize, K - i)?) }); let archetype_query = F::IS_ARCHETYPAL; @@ -2317,9 +2366,9 @@ struct QueryIterationCursor<'w, 's, D: QueryData, F: QueryFilter> { fetch: D::Fetch<'w>, filter: F::Fetch<'w>, // length of the table or length of the archetype, depending on whether both `D`'s and `F`'s fetches are dense - current_len: usize, + current_len: u32, // either table row or archetype index, depending on whether both `D`'s and `F`'s fetches are dense - current_row: usize, + current_row: u32, } impl Clone for QueryIterationCursor<'_, '_, D, F> { @@ -2395,30 +2444,34 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// The result of `next` and any previous calls to `peek_last` with this row must have been /// dropped to prevent aliasing mutable references. #[inline] - unsafe fn peek_last(&mut self) -> Option> { + unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> { if self.current_row > 0 { let index = self.current_row - 1; if self.is_dense { // SAFETY: This must have been called previously in `next` as `current_row > 0` - let entity = unsafe { self.table_entities.get_unchecked(index) }; + let entity = unsafe { self.table_entities.get_unchecked(index as usize) }; // SAFETY: // - `set_table` must have been called previously either in `next` or before it. // - `*entity` and `index` are in the current table. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, *entity, - TableRow::from_usize(index), + // SAFETY: This is from an exclusive range, so it can't be max. + TableRow::new(NonMaxU32::new_unchecked(index)), )) } } else { // SAFETY: This must have been called previously in `next` as `current_row > 0` - let archetype_entity = unsafe { self.archetype_entities.get_unchecked(index) }; + let archetype_entity = + unsafe { self.archetype_entities.get_unchecked(index as usize) }; // SAFETY: // - `set_archetype` must have been called previously either in `next` or before it. // - `archetype_entity.id()` and `archetype_entity.table_row()` are in the current archetype. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -2434,9 +2487,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// /// Note that if `F::IS_ARCHETYPAL`, the return value /// will be **the exact count of remaining values**. - fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> usize { + fn max_remaining(&self, tables: &'w Tables, archetypes: &'w Archetypes) -> u32 { let ids = self.storage_id_iter.clone(); - let remaining_matched: usize = if self.is_dense { + let remaining_matched: u32 = if self.is_dense { // SAFETY: The if check ensures that storage_id_iter stores TableIds unsafe { ids.map(|id| tables[id.table_id].entity_count()).sum() } } else { @@ -2460,7 +2513,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { tables: &'w Tables, archetypes: &'w Archetypes, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { if self.is_dense { loop { // we are on the beginning of the query, or finished processing a table, so skip to the next @@ -2483,9 +2536,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: set_table was called prior. // `current_row` is a table row in range of the current table, because if it was not, then the above would have been executed. - let entity = unsafe { self.table_entities.get_unchecked(self.current_row) }; - let row = TableRow::from_usize(self.current_row); - if !F::filter_fetch(&mut self.filter, *entity, row) { + let entity = + unsafe { self.table_entities.get_unchecked(self.current_row as usize) }; + // SAFETY: The row is less than the u32 len, so it must not be max. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(self.current_row)) }; + if !F::filter_fetch(&query_state.filter_state, &mut self.filter, *entity, row) { self.current_row += 1; continue; } @@ -2495,7 +2550,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `current_row` must be a table row in range of the current table, // because if it was not, then the above would have been executed. // - fetch is only called once for each `entity`. - let item = unsafe { D::fetch(&mut self.fetch, *entity, row) }; + let item = + unsafe { D::fetch(&query_state.fetch_state, &mut self.fetch, *entity, row) }; self.current_row += 1; return Some(item); @@ -2532,9 +2588,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // SAFETY: set_archetype was called prior. // `current_row` is an archetype index row in range of the current archetype, because if it was not, then the if above would have been executed. - let archetype_entity = - unsafe { self.archetype_entities.get_unchecked(self.current_row) }; + let archetype_entity = unsafe { + self.archetype_entities + .get_unchecked(self.current_row as usize) + }; if !F::filter_fetch( + &query_state.filter_state, &mut self.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -2550,6 +2609,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - fetch is only called once for each `archetype_entity`. let item = unsafe { D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index c1744cbf24..0bd3bbed23 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -107,7 +107,7 @@ mod tests { use crate::{ archetype::Archetype, component::{Component, ComponentId, Components, Tick}, - prelude::{AnyOf, Changed, Entity, Or, QueryState, Res, ResMut, Resource, With, Without}, + prelude::{AnyOf, Changed, Entity, Or, QueryState, Resource, With, Without}, query::{ ArchetypeFilter, FilteredAccess, Has, QueryCombinationIter, QueryData, ReadOnlyQueryData, WorldQuery, @@ -507,7 +507,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::query::tests::A conflicts with a previous access in this query."] + #[should_panic] fn self_conflicting_worldquery() { #[derive(QueryData)] #[query_data(mutable)] @@ -818,16 +818,15 @@ mod tests { /// SAFETY: /// `update_component_access` adds resource read access for `R`. - /// `update_archetype_component_access` does nothing, as this accesses no components. unsafe impl WorldQuery for ReadsRData { type Fetch<'w> = (); type State = ComponentId; fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -836,18 +835,18 @@ mod tests { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _table: &'w Table, ) { } @@ -883,16 +882,20 @@ mod tests { unsafe impl QueryData for ReadsRData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } @@ -904,28 +907,4 @@ mod tests { fn system(_q1: Query>, _q2: Query>) {} assert_is_system(system); } - - #[test] - fn read_res_sets_archetype_component_access() { - let mut world = World::new(); - - fn read_query(_q: Query>) {} - let mut read_query = IntoSystem::into_system(read_query); - read_query.initialize(&mut world); - - fn read_res(_r: Res) {} - let mut read_res = IntoSystem::into_system(read_res); - read_res.initialize(&mut world); - - fn write_res(_r: ResMut) {} - let mut write_res = IntoSystem::into_system(write_res); - write_res.initialize(&mut world); - - assert!(read_query - .archetype_component_access() - .is_compatible(read_res.archetype_component_access())); - assert!(!read_query - .archetype_component_access() - .is_compatible(write_res.archetype_component_access())); - } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index 6683238aa3..b8d8618fa5 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -39,7 +39,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -62,7 +62,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { /// query.par_iter().for_each_init(|| queue.borrow_local_mut(),|local_queue, item| { /// **local_queue += 1; /// }); - /// + /// /// // collect value from every thread /// let entity_count: usize = queue.iter_mut().map(|v| *v).sum(); /// } @@ -76,7 +76,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -130,7 +130,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { } #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - fn get_batch_size(&self, thread_count: usize) -> usize { + fn get_batch_size(&self, thread_count: usize) -> u32 { let max_items = || { let id_iter = self.state.matched_storage_ids.iter(); if self.state.is_dense { @@ -147,10 +147,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { .map(|id| unsafe { archetypes[id.archetype_id].len() }) .max() } + .map(|v| v as usize) .unwrap_or(0) }; self.batching_strategy - .calc_batch_size(max_items, thread_count) + .calc_batch_size(max_items, thread_count) as u32 } } @@ -189,7 +190,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -232,7 +233,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// query.par_iter_many(&entities).for_each_init(|| queue.borrow_local_mut(),|local_queue, item| { /// **local_queue += some_expensive_operation(item); /// }); - /// + /// /// // collect value from every thread /// let final_value: usize = queue.iter_mut().map(|v| *v).sum(); /// } @@ -246,7 +247,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -301,9 +302,9 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> } #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - fn get_batch_size(&self, thread_count: usize) -> usize { + fn get_batch_size(&self, thread_count: usize) -> u32 { self.batching_strategy - .calc_batch_size(|| self.entity_list.len(), thread_count) + .calc_batch_size(|| self.entity_list.len(), thread_count) as u32 } } @@ -344,7 +345,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -387,7 +388,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// query.par_iter_many_unique(&entities).for_each_init(|| queue.borrow_local_mut(),|local_queue, item| { /// **local_queue += some_expensive_operation(item); /// }); - /// + /// /// // collect value from every thread /// let final_value: usize = queue.iter_mut().map(|v| *v).sum(); /// } @@ -401,7 +402,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -456,8 +457,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> } #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - fn get_batch_size(&self, thread_count: usize) -> usize { + fn get_batch_size(&self, thread_count: usize) -> u32 { self.batching_strategy - .calc_batch_size(|| self.entity_list.len(), thread_count) + .calc_batch_size(|| self.entity_list.len(), thread_count) as u32 } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index cae8e592e7..00d8b6f970 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1,10 +1,10 @@ use crate::{ - archetype::{Archetype, ArchetypeComponentId, ArchetypeGeneration, ArchetypeId}, + archetype::{Archetype, ArchetypeGeneration, ArchetypeId}, component::{ComponentId, Tick}, entity::{Entity, EntityEquivalent, EntitySet, UniqueEntityArray}, entity_disabling::DefaultQueryFilters, prelude::FromWorld, - query::{Access, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, WorldQuery}, + query::{FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, WorldQuery}, storage::{SparseSetIndex, TableId}, system::Query, world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId}, @@ -14,6 +14,7 @@ use crate::{ use crate::entity::UniqueEntityEquivalentSlice; use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use core::{fmt, ptr}; use fixedbitset::FixedBitSet; use log::warn; @@ -21,8 +22,8 @@ use log::warn; use tracing::Span; use super::{ - ComponentAccessKind, NopWorldQuery, QueryBuilder, QueryData, QueryEntityError, QueryFilter, - QueryManyIter, QueryManyUniqueIter, QuerySingleError, ROQueryItem, ReadOnlyQueryData, + NopWorldQuery, QueryBuilder, QueryData, QueryEntityError, QueryFilter, QueryManyIter, + QueryManyUniqueIter, QuerySingleError, ROQueryItem, ReadOnlyQueryData, }; /// An ID for either a table or an archetype. Used for Query iteration. @@ -175,40 +176,6 @@ impl QueryState { Some(state) } - /// Identical to `new`, but it populates the provided `access` with the matched results. - pub(crate) fn new_with_access( - world: &mut World, - access: &mut Access, - ) -> Self { - let mut state = Self::new_uninitialized(world); - for archetype in world.archetypes.iter() { - // SAFETY: The state was just initialized from the `world` above, and the archetypes being added - // come directly from the same world. - unsafe { - if state.new_archetype_internal(archetype) { - state.update_archetype_component_access(archetype, access); - } - } - } - state.archetype_generation = world.archetypes.generation(); - - // Resource access is not part of any archetype and must be handled separately - if state.component_access.access().has_read_all_resources() { - access.read_all_resources(); - } else { - for component_id in state.component_access.access().resource_reads() { - access.add_resource_read(world.initialize_resource_internal(component_id).id()); - } - } - - debug_assert!( - !state.component_access.access().has_any_resource_write(), - "Mutable resource access in queries is not allowed" - ); - - 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 @@ -546,7 +513,7 @@ impl QueryState { // SAFETY: The validate_world call ensures that the world is the same the QueryState // was initialized from. unsafe { - self.new_archetype_internal(archetype); + self.new_archetype(archetype); } } } else { @@ -581,7 +548,7 @@ impl QueryState { // SAFETY: The validate_world call ensures that the world is the same the QueryState // was initialized from. unsafe { - self.new_archetype_internal(archetype); + self.new_archetype(archetype); } } } @@ -613,32 +580,9 @@ impl QueryState { /// Update the current [`QueryState`] with information from the provided [`Archetype`] /// (if applicable, i.e. if the archetype has any intersecting [`ComponentId`] with the current [`QueryState`]). /// - /// The passed in `access` will be updated with any new accesses introduced by the new archetype. - /// /// # Safety /// `archetype` must be from the `World` this state was initialized from. - pub unsafe fn new_archetype( - &mut self, - archetype: &Archetype, - access: &mut Access, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from. - let matches = unsafe { self.new_archetype_internal(archetype) }; - if matches { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from. - unsafe { self.update_archetype_component_access(archetype, access) }; - } - } - - /// Process the given [`Archetype`] to update internal metadata about the [`Table`](crate::storage::Table)s - /// and [`Archetype`]s that are matched by this query. - /// - /// Returns `true` if the given `archetype` matches the query. Otherwise, returns `false`. - /// If there is no match, then there is no need to update the query's [`FilteredAccess`]. - /// - /// # Safety - /// `archetype` must be from the `World` this state was initialized from. - unsafe fn new_archetype_internal(&mut self, archetype: &Archetype) -> bool { + pub unsafe fn new_archetype(&mut self, archetype: &Archetype) { if D::matches_component_set(&self.fetch_state, &|id| archetype.contains(id)) && F::matches_component_set(&self.filter_state, &|id| archetype.contains(id)) && self.matches_component_set(&|id| archetype.contains(id)) @@ -661,9 +605,6 @@ impl QueryState { }); } } - true - } else { - false } } @@ -680,57 +621,6 @@ impl QueryState { }) } - /// For the given `archetype`, adds any component accessed used by this query's underlying [`FilteredAccess`] to `access`. - /// - /// The passed in `access` will be updated with any new accesses introduced by the new archetype. - /// - /// # Safety - /// `archetype` must be from the `World` this state was initialized from. - pub unsafe fn update_archetype_component_access( - &mut self, - archetype: &Archetype, - access: &mut Access, - ) { - // As a fast path, we can iterate directly over the components involved - // if the `access` is finite. - if let Ok(iter) = self.component_access.access.try_iter_component_access() { - iter.for_each(|component_access| { - if let Some(id) = archetype.get_archetype_component_id(*component_access.index()) { - match component_access { - ComponentAccessKind::Archetypal(_) => {} - ComponentAccessKind::Shared(_) => { - access.add_component_read(id); - } - ComponentAccessKind::Exclusive(_) => { - access.add_component_write(id); - } - } - } - }); - - return; - } - - for (component_id, archetype_component_id) in - archetype.components_with_archetype_component_id() - { - if self - .component_access - .access - .has_component_read(component_id) - { - access.add_component_read(archetype_component_id); - } - if self - .component_access - .access - .has_component_write(component_id) - { - access.add_component_write(archetype_component_id); - } - } - } - /// Use this to transform a [`QueryState`] into a more generic [`QueryState`]. /// This can be useful for passing to another function that might take the more general form. /// See [`Query::transmute_lens`](crate::system::Query::transmute_lens) for more details. @@ -783,7 +673,7 @@ impl QueryState { assert!( component_access.is_subset(&self_access), "Transmuted state for {} attempts to access terms that are not allowed by original state {}.", - core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>() + DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>() ); QueryState { @@ -902,7 +792,7 @@ impl QueryState { assert!( component_access.is_subset(&joined_component_access), "Joined state for {} attempts to access terms that are not allowed by state {} joined with {}.", - core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>(), core::any::type_name::<(OtherD, OtherF)>() + DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>(), DebugName::type_name::<(OtherD, OtherF)>() ); if self.archetype_generation != other.archetype_generation { @@ -956,13 +846,17 @@ impl QueryState { /// /// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries. /// + /// If you need to get multiple items at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::get_manual`] calls, + /// or making a single call with [`Self::get_many`] or [`Self::iter_many`]. + /// /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get<'w>( &mut self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query(world).get_inner(entity) } @@ -1003,7 +897,7 @@ impl QueryState { &mut self, world: &'w World, entities: [Entity; N], - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_inner(entities) } @@ -1041,7 +935,7 @@ impl QueryState { &mut self, world: &'w World, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_unique_inner(entities) } @@ -1053,7 +947,7 @@ impl QueryState { &mut self, world: &'w mut World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_mut(world).get_inner(entity) } @@ -1100,7 +994,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_mut_inner(entities) } @@ -1145,7 +1039,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_unique_inner(entities) } @@ -1167,7 +1061,7 @@ impl QueryState { &self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_manual(world).get_inner(entity) } @@ -1184,13 +1078,16 @@ impl QueryState { &mut self, world: UnsafeWorldCell<'w>, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_unchecked(world).get_inner(entity) } /// Returns an [`Iterator`] over the query results for the given [`World`]. /// /// This can only be called for read-only queries, see [`Self::iter_mut`] for write-queries. + /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_manual`] calls. #[inline] pub fn iter<'w, 's>(&'s mut self, world: &'w World) -> QueryIter<'w, 's, D::ReadOnly, F> { self.query(world).into_iter() @@ -1279,6 +1176,9 @@ impl QueryState { /// Items are returned in the order of the list of entities. /// Entities that don't match the query are skipped. /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_many_manual`] calls. + /// /// # See also /// /// - [`iter_many_mut`](Self::iter_many_mut) to get mutable query items. @@ -1498,16 +1398,16 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, T, FN, INIT>( - &self, + pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, 's, T, FN, INIT>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, - batch_size: usize, + batch_size: u32, func: FN, last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: @@ -1516,7 +1416,7 @@ impl QueryState { use arrayvec::ArrayVec; bevy_tasks::ComputeTaskPool::get().scope(|scope| { - // SAFETY: We only access table data that has been registered in `self.archetype_component_access`. + // SAFETY: We only access table data that has been registered in `self.component_access`. let tables = unsafe { &world.storages().tables }; let archetypes = world.archetypes(); let mut batch_queue = ArrayVec::new(); @@ -1545,7 +1445,7 @@ impl QueryState { // submit single storage larger than batch_size let submit_single = |count, storage_id: StorageId| { - for offset in (0..count).step_by(batch_size) { + for offset in (0..count).step_by(batch_size as usize) { let mut func = func.clone(); let init_accum = init_accum.clone(); let len = batch_size.min(count - offset); @@ -1561,7 +1461,7 @@ impl QueryState { } }; - let storage_entity_count = |storage_id: StorageId| -> usize { + let storage_entity_count = |storage_id: StorageId| -> u32 { if self.is_dense { tables[storage_id.table_id].entity_count() } else { @@ -1612,17 +1512,17 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &UniqueEntityEquivalentSlice, - batch_size: usize, + batch_size: u32, mut func: FN, last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1631,7 +1531,7 @@ impl QueryState { // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual bevy_tasks::ComputeTaskPool::get().scope(|scope| { - let chunks = entity_list.chunks_exact(batch_size); + let chunks = entity_list.chunks_exact(batch_size as usize); let remainder = chunks.remainder(); for batch in chunks { @@ -1675,17 +1575,17 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &[E], - batch_size: usize, + batch_size: u32, mut func: FN, last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1694,7 +1594,7 @@ impl QueryState { // QueryState::par_many_fold_init_unchecked_manual, QueryState::par_many_unique_fold_init_unchecked_manual bevy_tasks::ComputeTaskPool::get().scope(|scope| { - let chunks = entity_list.chunks_exact(batch_size); + let chunks = entity_list.chunks_exact(batch_size as usize); let remainder = chunks.remainder(); for batch in chunks { @@ -1797,7 +1697,10 @@ impl QueryState { /// /// Simply unwrapping the [`Result`] also works, but should generally be reserved for tests. #[inline] - pub fn single<'w>(&mut self, world: &'w World) -> Result, QuerySingleError> { + pub fn single<'w>( + &mut self, + world: &'w World, + ) -> Result, QuerySingleError> { self.query(world).single_inner() } @@ -1814,7 +1717,7 @@ impl QueryState { pub fn single_mut<'w>( &mut self, world: &'w mut World, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_mut(world).single_inner() } @@ -1831,7 +1734,7 @@ impl QueryState { pub unsafe fn single_unchecked<'w>( &mut self, world: UnsafeWorldCell<'w>, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_unchecked(world).single_inner() } @@ -1853,7 +1756,7 @@ impl QueryState { world: UnsafeWorldCell<'w>, last_run: Tick, this_run: Tick, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { // SAFETY: // - The caller ensured we have the correct access to the world. // - The caller ensured that the world matches. @@ -1998,9 +1901,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for ((&bevy_ecs::query::state::tests::A, &bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_to_include_data_not_in_original_query() { let mut world = World::new(); world.register_component::(); @@ -2012,9 +1913,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_immut_to_mut() { let mut world = World::new(); world.spawn(A(0)); @@ -2024,9 +1923,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (core::option::Option<&bevy_ecs::query::state::tests::A>, ())." - )] + #[should_panic] fn cannot_transmute_option_to_immut() { let mut world = World::new(); world.spawn(C(0)); @@ -2038,9 +1935,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (bevy_ecs::world::entity_ref::EntityRef, ())." - )] + #[should_panic] fn cannot_transmute_entity_ref() { let mut world = World::new(); world.register_component::(); @@ -2106,9 +2001,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_changed_without_access() { let mut world = World::new(); world.register_component::(); @@ -2118,9 +2011,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Transmuted state for (&mut bevy_ecs::query::state::tests::A, ()) attempts to access terms that are not allowed by original state (&bevy_ecs::query::state::tests::A, ())." - )] + #[should_panic] fn cannot_transmute_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. @@ -2227,9 +2118,7 @@ mod tests { } #[test] - #[should_panic(expected = "Joined state for (&bevy_ecs::query::state::tests::C, ()) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, ()) joined with (&bevy_ecs::query::state::tests::B, ()).")] + #[should_panic] fn cannot_join_wrong_fetch() { let mut world = World::new(); world.register_component::(); @@ -2239,12 +2128,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for (bevy_ecs::entity::Entity, bevy_ecs::query::filter::Changed) \ - attempts to access terms that are not allowed by state \ - (&bevy_ecs::query::state::tests::A, bevy_ecs::query::filter::Without) \ - joined with (&bevy_ecs::query::state::tests::B, bevy_ecs::query::filter::Without)." - )] + #[should_panic] fn cannot_join_wrong_filter() { let mut world = World::new(); let query_1 = QueryState::<&A, Without>::new(&mut world); @@ -2253,9 +2137,7 @@ mod tests { } #[test] - #[should_panic( - expected = "Joined state for ((&mut bevy_ecs::query::state::tests::A, &mut bevy_ecs::query::state::tests::B), ()) attempts to access terms that are not allowed by state (&bevy_ecs::query::state::tests::A, ()) joined with (&mut bevy_ecs::query::state::tests::B, ())." - )] + #[should_panic] fn cannot_join_mutable_after_readonly() { let mut world = World::new(); // Calling this method would mean we had aliasing queries. diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index a6bcbf58bd..1c739927ac 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -42,7 +42,7 @@ use variadics_please::all_tuples; /// [`QueryFilter`]: crate::query::QueryFilter pub unsafe trait WorldQuery { /// Per archetype/table state retrieved by this [`WorldQuery`] to compute [`Self::Item`](crate::query::QueryData::Item) for each entity. - type Fetch<'a>: Clone; + type Fetch<'w>: Clone; /// State used to construct a [`Self::Fetch`](WorldQuery::Fetch). This will be cached inside [`QueryState`](crate::query::QueryState), /// so it is best to move as much data / computation here as possible to reduce the cost of @@ -62,9 +62,9 @@ pub unsafe trait WorldQuery { /// in to this function. /// - `world` must have the **right** to access any access registered in `update_component_access`. /// - There must not be simultaneous resource access conflicting with readonly resource access registered in [`WorldQuery::update_component_access`]. - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w>; @@ -87,9 +87,9 @@ pub unsafe trait WorldQuery { /// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `table` must correspond to `archetype`. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ); @@ -101,7 +101,11 @@ pub unsafe trait WorldQuery { /// /// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table); + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &'s Self::State, + table: &'w Table, + ); /// Adds any component accesses used by this [`WorldQuery`] to `access`. /// @@ -166,7 +170,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(unsafe { $name::init_fetch(world, $name, last_run, this_run) },)*) @@ -175,9 +179,9 @@ macro_rules! impl_tuple_world_query { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -188,7 +192,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($name,)*) = fetch; let ($($state,)*) = state; // SAFETY: The invariants are upheld by the caller. diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index ee02aff86e..133591c405 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -5,6 +5,7 @@ //! //! Same as [`super::component`], but for bundles. use alloc::boxed::Box; +use bevy_utils::prelude::DebugName; use core::any::{Any, TypeId}; use crate::{ @@ -172,7 +173,7 @@ impl FromType for Refl _ => panic!( "expected bundle `{}` to be named struct or tuple", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ), } } @@ -215,7 +216,7 @@ impl FromType for Refl _ => panic!( "expected bundle `{}` to be a named struct or tuple", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ), } } diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index 893e9b13fa..8a5c17179e 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -70,7 +70,7 @@ use crate::{ }, }; use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry}; -use disqualified::ShortName; +use bevy_utils::prelude::DebugName; /// A struct used to operate on reflected [`Component`] trait of a type. /// @@ -308,7 +308,8 @@ impl FromType for ReflectComponent { }, apply: |mut entity, reflected_component| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::apply` on component {name}. It is immutable, and cannot modified through reflection"); } @@ -357,7 +358,8 @@ impl FromType for ReflectComponent { reflect: |entity| entity.get::().map(|c| c as &dyn Reflect), reflect_mut: |entity| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::reflect_mut` on component {name}. It is immutable, and cannot modified through reflection"); } @@ -370,7 +372,8 @@ impl FromType for ReflectComponent { }, reflect_unchecked_mut: |entity| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::reflect_unchecked_mut` on component {name}. It is immutable, and cannot modified through reflection"); } diff --git a/crates/bevy_ecs/src/reflect/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs index 20c5e16c6d..6b5fad540e 100644 --- a/crates/bevy_ecs/src/reflect/entity_commands.rs +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -102,7 +102,7 @@ pub trait ReflectCommandExt { component: Box, ) -> &mut Self; - /// Removes from the entity the component or bundle with the given type name registered in [`AppTypeRegistry`]. + /// Removes from the entity the component or bundle with the given type path registered in [`AppTypeRegistry`]. /// /// If the type is a bundle, it will remove any components in that bundle regardless if the entity /// contains all the components. @@ -159,12 +159,12 @@ pub trait ReflectCommandExt { /// .remove_reflect(prefab.data.reflect_type_path().to_owned()); /// } /// ``` - fn remove_reflect(&mut self, component_type_name: impl Into>) -> &mut Self; + fn remove_reflect(&mut self, component_type_path: impl Into>) -> &mut Self; /// Same as [`remove_reflect`](ReflectCommandExt::remove_reflect), but using the `T` resource as type registry instead of /// `AppTypeRegistry`. fn remove_reflect_with_registry>( &mut self, - component_type_name: impl Into>, + component_type_path: impl Into>, ) -> &mut Self; } @@ -263,7 +263,7 @@ impl<'w> EntityWorldMut<'w> { self } - /// Removes from the entity the component or bundle with the given type name registered in [`AppTypeRegistry`]. + /// Removes from the entity the component or bundle with the given type path registered in [`AppTypeRegistry`]. /// /// If the type is a bundle, it will remove any components in that bundle regardless if the entity /// contains all the components. @@ -349,7 +349,7 @@ fn insert_reflect_with_registry_ref( .expect("component should represent a type."); let type_path = type_info.type_path(); let Ok(mut entity) = world.get_entity_mut(entity) else { - panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", + panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", world.entities().entity_does_not_exist_error_details(entity)); }; let Some(type_registration) = type_registry.get(type_info.type_id()) else { diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index b630f58719..24e1449e61 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -18,6 +18,7 @@ mod from_world; mod map_entities; mod resource; +use bevy_utils::prelude::DebugName; pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use entity_commands::ReflectCommandExt; @@ -136,7 +137,7 @@ pub fn from_reflect_with_fallback( `Default` or `FromWorld` traits. Are you perhaps missing a `#[reflect(Default)]` \ or `#[reflect(FromWorld)]`?", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ); }; diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 9a2a2a2d5a..8830998663 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -6,15 +6,16 @@ mod relationship_source_collection; use alloc::format; +use bevy_utils::prelude::DebugName; pub use related_methods::*; pub use relationship_query::*; pub use relationship_source_collection::*; use crate::{ - component::{Component, HookContext, Mutable}, + component::{Component, Mutable}, entity::{ComponentCloneCtx, Entity, SourceComponent}, error::{ignore, CommandWithEntity, HandleError}, - system::entity_command::{self}, + lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, }; use log::warn; @@ -81,6 +82,20 @@ pub trait Relationship: Component + Sized { /// Creates this [`Relationship`] from the given `entity`. fn from(entity: Entity) -> Self; + /// Changes the current [`Entity`] ID of the entity containing the [`RelationshipTarget`] to another one. + /// + /// This is useful for updating the relationship without overwriting other fields stored in `Self`. + /// + /// # Warning + /// + /// This should generally not be called by user code, as modifying the related entity could invalidate the + /// relationship. If this method is used, then the hooks [`on_replace`](Relationship::on_replace) have to + /// run before and [`on_insert`](Relationship::on_insert) after it. + /// This happens automatically when this method is called with [`EntityWorldMut::modify_component`]. + /// + /// Prefer to use regular means of insertions when possible. + fn set_risky(&mut self, entity: Entity); + /// The `on_insert` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. fn on_insert( mut world: DeferredWorld, @@ -105,8 +120,8 @@ pub trait Relationship: Component + Sized { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.", caller.map(|location|format!("{location}: ")).unwrap_or_default(), - core::any::type_name::(), - core::any::type_name::() + DebugName::type_name::(), + DebugName::type_name::() ); world.commands().entity(entity).remove::(); return; @@ -125,8 +140,8 @@ pub trait Relationship: Component + Sized { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.", caller.map(|location|format!("{location}: ")).unwrap_or_default(), - core::any::type_name::(), - core::any::type_name::() + DebugName::type_name::(), + DebugName::type_name::() ); world.commands().entity(entity).remove::(); } @@ -158,19 +173,21 @@ pub trait Relationship: Component + Sized { { relationship_target.collection_mut_risky().remove(entity); if relationship_target.len() == 0 { - if let Ok(mut entity) = world.commands().get_entity(target_entity) { + let command = |mut entity: EntityWorldMut| { // this "remove" operation must check emptiness because in the event that an identical // relationship is inserted on top, this despawn would result in the removal of that identical // relationship ... not what we want! - entity.queue(|mut entity: EntityWorldMut| { - if entity - .get::() - .is_some_and(RelationshipTarget::is_empty) - { - entity.remove::(); - } - }); - } + if entity + .get::() + .is_some_and(RelationshipTarget::is_empty) + { + entity.remove::(); + } + }; + + world + .commands() + .queue(command.with_entity(target_entity).handle_error_with(ignore)); } } } @@ -221,50 +238,24 @@ pub trait RelationshipTarget: Component + Sized { /// The `on_replace` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. // note: think of this as "on_drop" - fn on_replace(mut world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + fn on_replace(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - if entities.get(source_entity).is_ok() { - commands.queue( - entity_command::remove::() - .with_entity(source_entity) - .handle_error_with(ignore), - ); - } else { - warn!( - "{}Tried to despawn non-existent entity {}", - caller - .map(|location| format!("{location}: ")) - .unwrap_or_default(), - source_entity - ); - } + commands + .entity(source_entity) + .remove::(); } } /// The `on_despawn` component hook that despawns entities stored in an entity's [`RelationshipTarget`] when /// that entity is despawned. // note: think of this as "on_drop" - fn on_despawn(mut world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + fn on_despawn(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - if entities.get(source_entity).is_ok() { - commands.queue( - entity_command::despawn() - .with_entity(source_entity) - .handle_error_with(ignore), - ); - } else { - warn!( - "{}Tried to despawn non-existent entity {}", - caller - .map(|location| format!("{location}: ")) - .unwrap_or_default(), - source_entity - ); - } + commands.entity(source_entity).despawn(); } } @@ -424,4 +415,63 @@ mod tests { // No assert necessary, looking to make sure compilation works with the macros } + + #[test] + fn parent_child_relationship_with_custom_relationship() { + use crate::prelude::ChildOf; + + #[derive(Component)] + #[relationship(relationship_target = RelTarget)] + struct Rel(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Rel)] + struct RelTarget(Entity); + + let mut world = World::new(); + + // Rel on Parent + // Despawn Parent + let mut commands = world.commands(); + let child = commands.spawn_empty().id(); + let parent = commands.spawn(Rel(child)).add_child(child).id(); + commands.entity(parent).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(world.get_entity(parent).is_err()); + + // Rel on Parent + // Despawn Child + let mut commands = world.commands(); + let child = commands.spawn_empty().id(); + let parent = commands.spawn(Rel(child)).add_child(child).id(); + commands.entity(child).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(!world.entity(parent).contains::()); + + // Rel on Child + // Despawn Parent + let mut commands = world.commands(); + let parent = commands.spawn_empty().id(); + let child = commands.spawn((ChildOf(parent), Rel(parent))).id(); + commands.entity(parent).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(world.get_entity(parent).is_err()); + + // Rel on Child + // Despawn Child + let mut commands = world.commands(); + let parent = commands.spawn_empty().id(); + let child = commands.spawn((ChildOf(parent), Rel(parent))).id(); + commands.entity(child).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(!world.entity(parent).contains::()); + } } diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 5a23214463..8bae76a84e 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -1,11 +1,12 @@ use crate::{ bundle::Bundle, entity::{hash_set::EntityHashSet, Entity}, + prelude::Children, relationship::{ Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, }, system::{Commands, EntityCommands}, - world::{EntityWorldMut, World}, + world::{DeferredWorld, EntityWorldMut, World}, }; use bevy_platform::prelude::{Box, Vec}; use core::{marker::PhantomData, mem}; @@ -41,7 +42,12 @@ impl<'w> EntityWorldMut<'w> { let id = self.id(); self.world_scope(|world| { for related in related { - world.entity_mut(*related).insert(R::from(id)); + world + .entity_mut(*related) + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Run, + ); } }); self @@ -97,7 +103,12 @@ impl<'w> EntityWorldMut<'w> { .collection_mut_risky() .place(*related, index); } else { - world.entity_mut(*related).insert(R::from(id)); + world + .entity_mut(*related) + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Run, + ); world .get_mut::(id) .expect("hooks should have added relationship target") @@ -138,41 +149,46 @@ impl<'w> EntityWorldMut<'w> { return self; } - let Some(mut existing_relations) = self.get_mut::() else { + let Some(existing_relations) = self.get_mut::() else { return self.add_related::(related); }; - // We take the collection here so we can modify it without taking the component itself (this would create archetype move). + // We replace the component here with a dummy value so we can modify it without taking it (this would create archetype move). // SAFETY: We eventually return the correctly initialized collection into the target. - let mut existing_relations = mem::replace( - existing_relations.collection_mut_risky(), - Collection::::with_capacity(0), + let mut relations = mem::replace( + existing_relations.into_inner(), + ::RelationshipTarget::from_collection_risky( + Collection::::with_capacity(0), + ), ); + let collection = relations.collection_mut_risky(); + let mut potential_relations = EntityHashSet::from_iter(related.iter().copied()); let id = self.id(); self.world_scope(|world| { - for related in existing_relations.iter() { + for related in collection.iter() { if !potential_relations.remove(related) { world.entity_mut(related).remove::(); } } for related in potential_relations { - // SAFETY: We'll manually be adjusting the contents of the parent to fit the final state. + // SAFETY: We'll manually be adjusting the contents of the `RelationshipTarget` to fit the final state. world .entity_mut(related) - .insert_with_relationship_hook_mode(R::from(id), RelationshipHookMode::Skip); + .modify_or_insert_relation_with_relationship_hook_mode::( + id, + RelationshipHookMode::Skip, + ); } }); // SAFETY: The entities we're inserting will be the entities that were either already there or entities that we've just inserted. - existing_relations.clear(); - existing_relations.extend_from_iter(related.iter().copied()); - self.insert(R::RelationshipTarget::from_collection_risky( - existing_relations, - )); + collection.clear(); + collection.extend_from_iter(related.iter().copied()); + self.insert(relations); self } @@ -238,11 +254,20 @@ impl<'w> EntityWorldMut<'w> { assert_eq!(newly_related_entities, entities_to_relate, "`entities_to_relate` ({entities_to_relate:?}) didn't contain all entities that would end up related"); }; - if !self.contains::() { - self.add_related::(entities_to_relate); + match self.get_mut::() { + None => { + self.add_related::(entities_to_relate); - return self; - }; + return self; + } + Some(mut target) => { + // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. + let collection = target.collection_mut_risky(); + collection.clear(); + + collection.extend_from_iter(entities_to_relate.iter().copied()); + } + } let this = self.id(); self.world_scope(|world| { @@ -251,32 +276,16 @@ impl<'w> EntityWorldMut<'w> { } for new_relation in newly_related_entities { - // We're changing the target collection manually so don't run the insert hook + // We changed the target collection manually so don't run the insert hook world .entity_mut(*new_relation) - .insert_with_relationship_hook_mode(R::from(this), RelationshipHookMode::Skip); + .modify_or_insert_relation_with_relationship_hook_mode::( + this, + RelationshipHookMode::Skip, + ); } }); - if !entities_to_relate.is_empty() { - if let Some(mut target) = self.get_mut::() { - // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. - let collection = target.collection_mut_risky(); - collection.clear(); - - collection.extend_from_iter(entities_to_relate.iter().copied()); - } else { - let mut empty = - ::Collection::with_capacity( - entities_to_relate.len(), - ); - empty.extend_from_iter(entities_to_relate.iter().copied()); - - // SAFETY: We've just initialized this collection and we know there's no `RelationshipTarget` on `self` - self.insert(R::RelationshipTarget::from_collection_risky(empty)); - } - } - self } @@ -302,6 +311,15 @@ impl<'w> EntityWorldMut<'w> { self } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityWorldMut::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::(); + self + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -350,6 +368,40 @@ impl<'w> EntityWorldMut<'w> { self } + + fn modify_or_insert_relation_with_relationship_hook_mode( + &mut self, + entity: Entity, + relationship_hook_mode: RelationshipHookMode, + ) { + // Check if the relation edge holds additional data + if size_of::() > size_of::() { + self.assert_not_despawned(); + + let this = self.id(); + + let modified = self.world_scope(|world| { + let modified = DeferredWorld::from(&mut *world) + .modify_component_with_relationship_hook_mode::( + this, + relationship_hook_mode, + |r| r.set_risky(entity), + ) + .expect("entity access must be valid") + .is_some(); + + world.flush(); + + modified + }); + + if modified { + return; + } + } + + self.insert_with_relationship_hook_mode(R::from(entity), relationship_hook_mode); + } } impl<'a> EntityCommands<'a> { @@ -467,6 +519,14 @@ impl<'a> EntityCommands<'a> { }) } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityCommands::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::() + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -650,4 +710,176 @@ mod tests { assert_eq!(world.entity(b).get::(), None); assert_eq!(world.entity(c).get::(), None); } + + #[test] + fn replace_related_works() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut parent = world.spawn_empty(); + parent.add_children(&[child1, child2]); + let child_value = ChildOf(parent.id()); + let some_child = Some(&child_value); + + parent.replace_children(&[child2, child3]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child2, child3]); + assert_eq!(parent.world().get::(child1), None); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), some_child); + + parent.replace_children_with_difference(&[child3], &[child1, child2], &[child1]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child1, child2]); + assert_eq!(parent.world().get::(child1), some_child); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), None); + } + + #[test] + fn add_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world.entity_mut(parent2).add_related::(&[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + } + + #[test] + fn insert_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world + .entity_mut(parent2) + .insert_related::(0, &[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + } + + #[test] + fn replace_related_keeps_relationship_data() { + #[derive(Component, PartialEq, Debug)] + #[relationship(relationship_target = Parent)] + struct Child { + #[relationship] + parent: Entity, + data: u8, + } + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent(Vec); + + let mut world = World::new(); + let parent1 = world.spawn_empty().id(); + let parent2 = world.spawn_empty().id(); + let child = world + .spawn(Child { + parent: parent1, + data: 42, + }) + .id(); + + world + .entity_mut(parent2) + .replace_related_with_difference::(&[], &[child], &[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent2, + data: 42 + }) + ); + + world.entity_mut(parent1).replace_related::(&[child]); + assert_eq!( + world.get::(child), + Some(&Child { + parent: parent1, + data: 42 + }) + ); + } + + #[test] + fn replace_related_keeps_relationship_target_data() { + #[derive(Component)] + #[relationship(relationship_target = Parent)] + struct Child(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Child)] + struct Parent { + #[relationship] + children: Vec, + data: u8, + } + + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let mut parent = world.spawn_empty(); + parent.add_related::(&[child1]); + parent.get_mut::().unwrap().data = 42; + + parent.replace_related_with_difference::(&[child1], &[child2], &[child2]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + + parent.replace_related::(&[child1]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + } } diff --git a/crates/bevy_ecs/src/relationship/relationship_query.rs b/crates/bevy_ecs/src/relationship/relationship_query.rs index a2ec937c29..a7acea7de0 100644 --- a/crates/bevy_ecs/src/relationship/relationship_query.rs +++ b/crates/bevy_ecs/src/relationship/relationship_query.rs @@ -14,7 +14,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// target entity of that relationship. pub fn related(&'w self, entity: Entity) -> Option where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { self.get(entity).map(R::get).ok() } @@ -26,7 +26,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, { self.get(entity) .into_iter() @@ -42,7 +42,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity. pub fn root_ancestor(&'w self, entity: Entity) -> Entity where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { // Recursively search up the tree until we're out of parents match self.get(entity) { @@ -60,9 +60,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn iter_leaves( &'w self, entity: Entity, - ) -> impl Iterator + 'w + ) -> impl Iterator + use<'w, 's, S, D, F> where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { self.iter_descendants_depth_first(entity).filter(|entity| { @@ -80,7 +80,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, + D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, { self.get(entity) .ok() @@ -103,7 +103,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { DescendantIter::new(self, entity) } @@ -120,7 +120,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { DescendantDepthFirstIter::new(self, entity) @@ -137,7 +137,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { AncestorIter::new(self, entity) } @@ -148,7 +148,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Traverses the hierarchy breadth-first. pub struct DescendantIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, vecdeque: VecDeque, @@ -156,7 +156,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { /// Returns a new [`DescendantIter`]. pub fn new(children_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -174,7 +174,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { type Item = Entity; @@ -194,7 +194,7 @@ where /// Traverses the hierarchy depth-first. pub struct DescendantDepthFirstIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, stack: SmallVec<[Entity; 8]>, @@ -203,7 +203,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { /// Returns a new [`DescendantDepthFirstIter`]. @@ -220,7 +220,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { type Item = Entity; @@ -239,7 +239,7 @@ where /// An [`Iterator`] of [`Entity`]s over the ancestors of an [`Entity`]. pub struct AncestorIter<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { parent_query: &'w Query<'w, 's, D, F>, next: Option, @@ -247,7 +247,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { /// Returns a new [`AncestorIter`]. pub fn new(parent_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -261,7 +261,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> Iterator for AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { type Item = Entity; diff --git a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs index d4ea45f64f..01c9edf41b 100644 --- a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs +++ b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs @@ -86,13 +86,13 @@ pub trait OrderedRelationshipSourceCollection: RelationshipSourceCollection { /// Inserts the entity at a specific index. /// If the index is too large, the entity will be added to the end of the collection. fn insert(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. fn remove_at(&mut self, index: usize) -> Option; /// Inserts the entity at a specific index. /// This will never reorder other entities. /// If the index is too large, the entity will be added to the end of the collection. fn insert_stable(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. /// This will never reorder other entities. fn remove_at_stable(&mut self, index: usize) -> Option; /// Sorts the source collection. diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs deleted file mode 100644 index 64cc63a7ce..0000000000 --- a/crates/bevy_ecs/src/removal_detection.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Alerting events when a component is removed from an entity. - -use crate::{ - component::{Component, ComponentId, ComponentIdFor, Tick}, - entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, - prelude::Local, - storage::SparseSet, - system::{ReadOnlySystemParam, SystemMeta, SystemParam}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, -}; - -use derive_more::derive::Into; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; -use core::{ - fmt::Debug, - iter, - marker::PhantomData, - ops::{Deref, DerefMut}, - option, -}; - -/// Wrapper around [`Entity`] for [`RemovedComponents`]. -/// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] -pub struct RemovedComponentEntity(Entity); - -/// Wrapper around a [`EventCursor`] so that we -/// can differentiate events between components. -#[derive(Debug)] -pub struct RemovedComponentReader -where - T: Component, -{ - reader: EventCursor, - marker: PhantomData, -} - -impl Default for RemovedComponentReader { - fn default() -> Self { - Self { - reader: Default::default(), - marker: PhantomData, - } - } -} - -impl Deref for RemovedComponentReader { - type Target = EventCursor; - fn deref(&self) -> &Self::Target { - &self.reader - } -} - -impl DerefMut for RemovedComponentReader { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.reader - } -} - -/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. -#[derive(Default, Debug)] -pub struct RemovedComponentEvents { - event_sets: SparseSet>, -} - -impl RemovedComponentEvents { - /// Creates an empty storage buffer for component removal events. - pub fn new() -> Self { - Self::default() - } - - /// For each type of component, swaps the event buffers and clears the oldest event buffer. - /// In general, this should be called once per frame/update. - pub fn update(&mut self) { - for (_component_id, events) in self.event_sets.iter_mut() { - events.update(); - } - } - - /// Returns an iterator over components and their entity events. - pub fn iter(&self) -> impl Iterator)> { - self.event_sets.iter() - } - - /// Gets the event storage for a given component. - pub fn get( - &self, - component_id: impl Into, - ) -> Option<&Events> { - self.event_sets.get(component_id.into()) - } - - /// Sends a removal event for the specified component. - pub fn send(&mut self, component_id: impl Into, entity: Entity) { - self.event_sets - .get_or_insert_with(component_id.into(), Default::default) - .send(RemovedComponentEntity(entity)); - } -} - -/// A [`SystemParam`] that yields entities that had their `T` [`Component`] -/// removed or have been despawned with it. -/// -/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). -/// -/// Note that this does not allow you to see which data existed before removal. -/// If you need this, you will need to track the component data value on your own, -/// using a regularly scheduled system that requests `Query<(Entity, &T), Changed>` -/// and stores the data somewhere safe to later cross-reference. -/// -/// If you are using `bevy_ecs` as a standalone crate, -/// note that the `RemovedComponents` list will not be automatically cleared for you, -/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). -/// -/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is -/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. -/// For the main world, this is delayed until after all `SubApp`s have run. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::removal_detection::RemovedComponents; -/// # -/// # #[derive(Component)] -/// # struct MyComponent; -/// fn react_on_removal(mut removed: RemovedComponents) { -/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); -/// } -/// # bevy_ecs::system::assert_is_system(react_on_removal); -/// ``` -#[derive(SystemParam)] -pub struct RemovedComponents<'w, 's, T: Component> { - component_id: ComponentIdFor<'s, T>, - reader: Local<'s, RemovedComponentReader>, - event_sets: &'w RemovedComponentEvents, -} - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIter<'a> = iter::Map< - iter::Flatten>>>, - fn(RemovedComponentEntity) -> Entity, ->; - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIterWithId<'a> = iter::Map< - iter::Flatten>>, - fn( - (&RemovedComponentEntity, EventId), - ) -> (Entity, EventId), ->; - -fn map_id_events( - (entity, id): (&RemovedComponentEntity, EventId), -) -> (Entity, EventId) { - (entity.clone().into(), id) -} - -// For all practical purposes, the api surface of `RemovedComponents` -// should be similar to `EventReader` to reduce confusion. -impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { - /// Fetch underlying [`EventCursor`]. - pub fn reader(&self) -> &EventCursor { - &self.reader - } - - /// Fetch underlying [`EventCursor`] mutably. - pub fn reader_mut(&mut self) -> &mut EventCursor { - &mut self.reader - } - - /// Fetch underlying [`Events`]. - pub fn events(&self) -> Option<&Events> { - self.event_sets.get(self.component_id.get()) - } - - /// Destructures to get a mutable reference to the `EventCursor` - /// and a reference to `Events`. - /// - /// This is necessary since Rust can't detect destructuring through methods and most - /// usecases of the reader uses the `Events` as well. - pub fn reader_mut_with_events( - &mut self, - ) -> Option<( - &mut RemovedComponentReader, - &Events, - )> { - self.event_sets - .get(self.component_id.get()) - .map(|events| (&mut *self.reader, events)) - } - - /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the - /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events - /// that happened before now. - pub fn read(&mut self) -> RemovedIter<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read(events).cloned()) - .into_iter() - .flatten() - .map(RemovedComponentEntity::into) - } - - /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. - pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read_with_id(events)) - .into_iter() - .flatten() - .map(map_id_events) - } - - /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. - pub fn len(&self) -> usize { - self.events() - .map(|events| self.reader.len(events)) - .unwrap_or(0) - } - - /// Returns `true` if there are no events available to read. - pub fn is_empty(&self) -> bool { - self.events() - .is_none_or(|events| self.reader.is_empty(events)) - } - - /// Consumes all available events. - /// - /// This means these events will not appear in calls to [`RemovedComponents::read()`] or - /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. - pub fn clear(&mut self) { - if let Some((reader, events)) = self.reader_mut_with_events() { - reader.clear(events); - } - } -} - -// SAFETY: Only reads World removed component events -unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} - -// SAFETY: no component value access. -unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { - type State = (); - type Item<'w, 's> = &'w RemovedComponentEvents; - - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - - #[inline] - unsafe fn get_param<'w, 's>( - _state: &'s mut Self::State, - _system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - _change_tick: Tick, - ) -> Self::Item<'w, 's> { - world.removed_components() - } -} diff --git a/crates/bevy_ecs/src/resource.rs b/crates/bevy_ecs/src/resource.rs index c3f7805631..7da4f31113 100644 --- a/crates/bevy_ecs/src/resource.rs +++ b/crates/bevy_ecs/src/resource.rs @@ -54,7 +54,7 @@ pub use bevy_ecs_macros::Resource; /// ``` /// # use std::cell::RefCell; /// # use bevy_ecs::resource::Resource; -/// use bevy_utils::synccell::SyncCell; +/// use bevy_platform::cell::SyncCell; /// /// #[derive(Resource)] /// struct ActuallySync { @@ -66,7 +66,7 @@ pub use bevy_ecs_macros::Resource; /// [`World`]: crate::world::World /// [`Res`]: crate::system::Res /// [`ResMut`]: crate::system::ResMut -/// [`SyncCell`]: bevy_utils::synccell::SyncCell +/// [`SyncCell`]: bevy_platform::cell::SyncCell #[diagnostic::on_unimplemented( message = "`{Self}` is not a `Resource`", label = "invalid `Resource`", diff --git a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs index dda6d604a7..997259e104 100644 --- a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs +++ b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs @@ -102,7 +102,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut system_has_conditions_cache = HashMap::::default(); let mut is_valid_explicit_sync_point = |system: NodeId| { let index = system.index(); - is_apply_deferred(graph.systems[index].get().unwrap()) + is_apply_deferred(&graph.systems[index].get().unwrap().system) && !*system_has_conditions_cache .entry(index) .or_insert_with(|| system_has_conditions(graph, system)) @@ -138,7 +138,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { } else if !node_needs_sync { // No previous node has postponed sync points to add so check if the system itself // has deferred params that require a sync point to apply them. - node_needs_sync = graph.systems[node.index()].get().unwrap().has_deferred(); + node_needs_sync = graph.systems[node.index()] + .get() + .unwrap() + .system + .has_deferred(); } for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) { @@ -148,7 +152,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut edge_needs_sync = node_needs_sync; if node_needs_sync - && !graph.systems[target.index()].get().unwrap().is_exclusive() + && !graph.systems[target.index()] + .get() + .unwrap() + .system + .is_exclusive() && self.no_sync_edges.contains(&(*node, target)) { // The node has deferred params to apply, but this edge is ignoring sync points. @@ -190,7 +198,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { continue; } - if is_apply_deferred(graph.systems[target.index()].get().unwrap()) { + if is_apply_deferred(&graph.systems[target.index()].get().unwrap().system) { // We don't need to insert a sync point since ApplyDeferred is a sync point // already! continue; diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index a85a8c6fa4..9a7ce5d507 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1,4 +1,5 @@ -use alloc::{borrow::Cow, boxed::Box, format}; +use alloc::{boxed::Box, format}; +use bevy_utils::prelude::DebugName; use core::ops::Not; use crate::system::{ @@ -11,30 +12,40 @@ pub type BoxedCondition = Box>; /// A system that determines if one or more scheduled systems should run. /// -/// Implemented for functions and closures that convert into [`System`](System) -/// with [read-only](crate::system::ReadOnlySystemParam) parameters. +/// `SystemCondition` is sealed and implemented for functions and closures with +/// [read-only](crate::system::ReadOnlySystemParam) parameters that convert into +/// [`System`](System), [`System>`](System) or +/// [`System>`](System). +/// +/// `SystemCondition` offers a private method +/// (called by [`run_if`](crate::schedule::IntoScheduleConfigs::run_if) and the provided methods) +/// that converts the implementing system into a condition (system) returning a bool. +/// Depending on the output type of the implementing system: +/// - `bool`: the implementing system is used as the condition; +/// - `Result<(), BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(())`; +/// - `Result`: the condition returns `true` if and only if the implementing system returns `Ok(true)`. /// /// # Marker type parameter /// -/// `Condition` trait has `Marker` type parameter, which has no special meaning, +/// `SystemCondition` trait has `Marker` type parameter, which has no special meaning, /// but exists to work around the limitation of Rust's trait system. /// /// Type parameter in return type can be set to `<()>` by calling [`IntoSystem::into_system`], /// but usually have to be specified when passing a condition to a function. /// /// ``` -/// # use bevy_ecs::schedule::Condition; +/// # use bevy_ecs::schedule::SystemCondition; /// # use bevy_ecs::system::IntoSystem; -/// fn not_condition(a: impl Condition) -> impl Condition<()> { +/// fn not_condition(a: impl SystemCondition) -> impl SystemCondition<()> { /// IntoSystem::into_system(a.map(|x| !x)) /// } /// ``` /// /// # Examples -/// A condition that returns true every other time it's called. +/// A condition that returns `true` every other time it's called. /// ``` /// # use bevy_ecs::prelude::*; -/// fn every_other_time() -> impl Condition<()> { +/// fn every_other_time() -> impl SystemCondition<()> { /// IntoSystem::into_system(|mut flag: Local| { /// *flag = !*flag; /// *flag @@ -54,11 +65,11 @@ pub type BoxedCondition = Box>; /// # assert!(!world.resource::().0); /// ``` /// -/// A condition that takes a bool as an input and returns it unchanged. +/// A condition that takes a `bool` as an input and returns it unchanged. /// /// ``` /// # use bevy_ecs::prelude::*; -/// fn identity() -> impl Condition<(), In> { +/// fn identity() -> impl SystemCondition<(), In> { /// IntoSystem::into_system(|In(x)| x) /// } /// @@ -71,7 +82,31 @@ pub type BoxedCondition = Box>; /// # world.insert_resource(DidRun(false)); /// # app.run(&mut world); /// # assert!(world.resource::().0); -pub trait Condition: sealed::Condition { +/// ``` +/// +/// A condition returning a `Result<(), BevyError>` +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] struct Player; +/// fn player_exists(q_player: Query<(), With>) -> Result { +/// Ok(q_player.single()?) +/// } +/// +/// # let mut app = Schedule::default(); +/// # #[derive(Resource)] struct DidRun(bool); +/// # fn my_system(mut did_run: ResMut) { did_run.0 = true; } +/// app.add_systems(my_system.run_if(player_exists)); +/// # let mut world = World::new(); +/// # world.insert_resource(DidRun(false)); +/// # app.run(&mut world); +/// # assert!(!world.resource::().0); +/// # world.spawn(Player); +/// # app.run(&mut world); +/// # assert!(world.resource::().0); +pub trait SystemCondition: + sealed::SystemCondition +{ /// Returns a new run condition that only returns `true` /// if both this one and the passed `and` return `true`. /// @@ -116,11 +151,11 @@ pub trait Condition: sealed::Condition /// Note that in this case, it's better to just use the run condition [`resource_exists_and_equals`]. /// /// [`resource_exists_and_equals`]: common_conditions::resource_exists_and_equals - fn and>(self, and: C) -> And { + fn and>(self, and: C) -> And { let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(and); let name = format!("{} && {}", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `false` @@ -168,11 +203,11 @@ pub trait Condition: sealed::Condition /// ), /// ); /// ``` - fn nand>(self, nand: C) -> Nand { + fn nand>(self, nand: C) -> Nand { let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(nand); let name = format!("!({} && {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -220,11 +255,11 @@ pub trait Condition: sealed::Condition /// ), /// ); /// ``` - fn nor>(self, nor: C) -> Nor { + fn nor>(self, nor: C) -> Nor { let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(nor); let name = format!("!({} || {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that returns `true` @@ -267,11 +302,11 @@ pub trait Condition: sealed::Condition /// # app.run(&mut world); /// # assert!(world.resource::().0); /// ``` - fn or>(self, or: C) -> Or { + fn or>(self, or: C) -> Or { let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(or); let name = format!("{} || {}", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -319,11 +354,11 @@ pub trait Condition: sealed::Condition /// ), /// ); /// ``` - fn xnor>(self, xnor: C) -> Xnor { + fn xnor>(self, xnor: C) -> Xnor { let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(xnor); let name = format!("!({} ^ {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -361,51 +396,87 @@ pub trait Condition: sealed::Condition /// ); /// # app.run(&mut world); /// ``` - fn xor>(self, xor: C) -> Xor { + fn xor>(self, xor: C) -> Xor { let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(xor); let name = format!("({} ^ {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } } -impl Condition for F where F: sealed::Condition {} +impl SystemCondition for F where + F: sealed::SystemCondition +{ +} mod sealed { - use crate::system::{IntoSystem, ReadOnlySystem, SystemInput}; + use crate::{ + error::BevyError, + system::{IntoSystem, ReadOnlySystem, SystemInput}, + }; - pub trait Condition: - IntoSystem + pub trait SystemCondition: + IntoSystem { // This associated type is necessary to let the compiler // know that `Self::System` is `ReadOnlySystem`. - type ReadOnlySystem: ReadOnlySystem; + type ReadOnlySystem: ReadOnlySystem; + + fn into_condition_system(self) -> impl ReadOnlySystem; } - impl Condition for F + impl SystemCondition for F where F: IntoSystem, F::System: ReadOnlySystem, { type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| result.is_ok())) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| matches!(result, Ok(true)))) + } } } -/// A collection of [run conditions](Condition) that may be useful in any bevy app. +/// A collection of [run conditions](SystemCondition) that may be useful in any bevy app. pub mod common_conditions { - use super::{Condition, NotSystem}; + use super::{NotSystem, SystemCondition}; use crate::{ change_detection::DetectChanges, - event::{Event, EventReader}, + event::{BufferedEvent, EventReader}, + lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, - removal_detection::RemovedComponents, resource::Resource, system::{In, IntoSystem, Local, Res, System, SystemInput}, }; use alloc::format; - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// on the first time the condition is run and false every time after. /// /// # Example @@ -443,7 +514,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if the resource exists. /// /// # Example @@ -478,7 +549,7 @@ pub mod common_conditions { res.is_some() } - /// Generates a [`Condition`]-satisfying closure that returns `true` + /// Generates a [`SystemCondition`]-satisfying closure that returns `true` /// if the resource is equal to `value`. /// /// # Panics @@ -518,7 +589,7 @@ pub mod common_conditions { move |res: Res| *res == value } - /// Generates a [`Condition`]-satisfying closure that returns `true` + /// Generates a [`SystemCondition`]-satisfying closure that returns `true` /// if the resource exists and is equal to `value`. /// /// The condition will return `false` if the resource does not exist. @@ -563,7 +634,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if the resource of the given type has been added since the condition was last checked. /// /// # Example @@ -604,13 +675,12 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` - /// if the resource of the given type has had its value changed since the condition - /// was last checked. + /// A [`SystemCondition`]-satisfying system that returns `true` + /// if the resource of the given type has been added or mutably dereferenced + /// since the condition was last checked. /// - /// The value is considered changed when it is added. The first time this condition - /// is checked after the resource was added, it will return `true`. - /// Change detection behaves like this everywhere in Bevy. + /// **Note** that simply *mutably dereferencing* a resource is considered a change ([`DerefMut`](std::ops::DerefMut)). + /// Bevy does not compare resources to their previous values. /// /// # Panics /// @@ -658,15 +728,12 @@ pub mod common_conditions { res.is_changed() } - /// A [`Condition`]-satisfying system that returns `true` - /// if the resource of the given type has had its value changed since the condition + /// A [`SystemCondition`]-satisfying system that returns `true` + /// if the resource of the given type has been added or mutably dereferenced since the condition /// was last checked. /// - /// The value is considered changed when it is added. The first time this condition - /// is checked after the resource was added, it will return `true`. - /// Change detection behaves like this everywhere in Bevy. - /// - /// This run condition does not detect when the resource is removed. + /// **Note** that simply *mutably dereferencing* a resource is considered a change ([`DerefMut`](std::ops::DerefMut)). + /// Bevy does not compare resources to their previous values. /// /// The condition will return `false` if the resource does not exist. /// @@ -718,16 +785,12 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` - /// if the resource of the given type has had its value changed since the condition + /// A [`SystemCondition`]-satisfying system that returns `true` + /// if the resource of the given type has been added, removed or mutably dereferenced since the condition /// was last checked. /// - /// The value is considered changed when it is added. The first time this condition - /// is checked after the resource was added, it will return `true`. - /// Change detection behaves like this everywhere in Bevy. - /// - /// This run condition also detects removal. It will return `true` if the resource - /// has been removed since the run condition was last checked. + /// **Note** that simply *mutably dereferencing* a resource is considered a change ([`DerefMut`](std::ops::DerefMut)). + /// Bevy does not compare resources to their previous values. /// /// The condition will return `false` if the resource does not exist. /// @@ -795,7 +858,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if the resource of the given type has been removed since the condition was last checked. /// /// # Example @@ -847,7 +910,7 @@ pub mod common_conditions { } } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any new events of the given type since it was last called. /// /// # Example @@ -866,7 +929,7 @@ pub mod common_conditions { /// my_system.run_if(on_event::), /// ); /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent; /// /// fn my_system(mut counter: ResMut) { @@ -883,7 +946,7 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn on_event(mut reader: EventReader) -> bool { + pub fn on_event(mut reader: EventReader) -> bool { // The events need to be consumed, so that there are no false positives on subsequent // calls of the run condition. Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), @@ -891,7 +954,7 @@ pub mod common_conditions { reader.read().count() > 0 } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any entities with the given component type. /// /// # Example @@ -928,7 +991,7 @@ pub mod common_conditions { !query.is_empty() } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any entity with a component of the given type removed. pub fn any_component_removed(mut removals: RemovedComponents) -> bool { // `RemovedComponents` based on events and therefore events need to be consumed, @@ -939,13 +1002,13 @@ pub mod common_conditions { removals.read().count() > 0 } - /// A [`Condition`]-satisfying system that returns `true` + /// A [`SystemCondition`]-satisfying system that returns `true` /// if there are any entities that match the given [`QueryFilter`]. pub fn any_match_filter(query: Query<(), F>) -> bool { !query.is_empty() } - /// Generates a [`Condition`] that inverses the result of passed one. + /// Generates a [`SystemCondition`] that inverses the result of passed one. /// /// # Example /// @@ -984,7 +1047,7 @@ pub mod common_conditions { NotSystem::new(super::NotMarker, condition, name.into()) } - /// Generates a [`Condition`] that returns true when the passed one changes. + /// Generates a [`SystemCondition`] that returns true when the passed one changes. /// /// The first time this is called, the passed condition is assumed to have been previously false. /// @@ -1022,10 +1085,10 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 2); /// ``` - pub fn condition_changed(condition: C) -> impl Condition<(), CIn> + pub fn condition_changed(condition: C) -> impl SystemCondition<(), CIn> where CIn: SystemInput, - C: Condition, + C: SystemCondition, { IntoSystem::into_system(condition.pipe(|In(new): In, mut prev: Local| { let changed = *prev != new; @@ -1034,7 +1097,7 @@ pub mod common_conditions { })) } - /// Generates a [`Condition`] that returns true when the result of + /// Generates a [`SystemCondition`] that returns true when the result of /// the passed one went from false to true since the last time this was called. /// /// The first time this is called, the passed condition is assumed to have been previously false. @@ -1078,10 +1141,13 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 2); /// ``` - pub fn condition_changed_to(to: bool, condition: C) -> impl Condition<(), CIn> + pub fn condition_changed_to( + to: bool, + condition: C, + ) -> impl SystemCondition<(), CIn> where CIn: SystemInput, - C: Condition, + C: SystemCondition, { IntoSystem::into_system(condition.pipe( move |In(new): In, mut prev: Local| -> bool { @@ -1262,7 +1328,8 @@ where #[cfg(test)] mod tests { - use super::{common_conditions::*, Condition}; + use super::{common_conditions::*, SystemCondition}; + use crate::event::{BufferedEvent, Event}; use crate::query::With; use crate::{ change_detection::ResMut, @@ -1271,7 +1338,7 @@ mod tests { system::Local, world::World, }; - use bevy_ecs_macros::{Event, Resource}; + use bevy_ecs_macros::Resource; #[derive(Resource, Default)] struct Counter(usize); @@ -1382,7 +1449,7 @@ mod tests { #[derive(Component)] struct TestComponent; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent; #[derive(Resource)] diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index b98205e32b..4826d0a66d 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -6,7 +6,7 @@ use crate::{ never::Never, schedule::{ auto_insert_apply_deferred::IgnoreDeferred, - condition::{BoxedCondition, Condition}, + condition::{BoxedCondition, SystemCondition}, graph::{Ambiguity, Dependency, DependencyKind, GraphInfo}, set::{InternedSystemSet, IntoSystemSet, SystemSet}, Chain, @@ -14,11 +14,11 @@ use crate::{ system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System}, }; -fn new_condition(condition: impl Condition) -> BoxedCondition { - let condition_system = IntoSystem::into_system(condition); +fn new_condition(condition: impl SystemCondition) -> BoxedCondition { + let condition_system = condition.into_condition_system(); assert!( condition_system.is_send(), - "Condition `{}` accesses `NonSend` resources. This is not currently supported.", + "SystemCondition `{}` accesses `NonSend` resources. This is not currently supported.", condition_system.name() ); @@ -191,7 +191,7 @@ impl> ScheduleConfig } } - fn distributive_run_if_inner(&mut self, condition: impl Condition + Clone) { + fn distributive_run_if_inner(&mut self, condition: impl SystemCondition + Clone) { match self { Self::ScheduleConfig(config) => { config.conditions.push(new_condition(condition)); @@ -382,8 +382,8 @@ pub trait IntoScheduleConfigs(self, condition: impl Condition + Clone) -> ScheduleConfigs { + fn distributive_run_if( + self, + condition: impl SystemCondition + Clone, + ) -> ScheduleConfigs { self.into_configs().distributive_run_if(condition) } - /// Run the systems only if the [`Condition`] is `true`. + /// Run the systems only if the [`SystemCondition`] is `true`. /// - /// The `Condition` will be evaluated at most once (per schedule run), + /// The `SystemCondition` will be evaluated at most once (per schedule run), /// the first time a system in this set prepares to run. /// /// If this set contains more than one system, calling `run_if` is equivalent to adding each @@ -444,7 +447,7 @@ pub trait IntoScheduleConfigs(self, condition: impl Condition) -> ScheduleConfigs { + fn run_if(self, condition: impl SystemCondition) -> ScheduleConfigs { self.into_configs().run_if(condition) } @@ -526,13 +529,13 @@ impl> IntoScheduleCo fn distributive_run_if( mut self, - condition: impl Condition + Clone, + condition: impl SystemCondition + Clone, ) -> ScheduleConfigs { self.distributive_run_if_inner(condition); self } - fn run_if(mut self, condition: impl Condition) -> ScheduleConfigs { + fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { self.run_if_dyn(new_condition(condition)); self } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index a601284fb0..9156fc34a3 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -3,7 +3,8 @@ mod multi_threaded; mod simple; mod single_threaded; -use alloc::{borrow::Cow, vec, vec::Vec}; +use alloc::{vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::any::TypeId; #[expect(deprecated, reason = "We still need to support this.")] @@ -15,13 +16,12 @@ pub use self::multi_threaded::{MainThreadExecutor, MultiThreadedExecutor}; use fixedbitset::FixedBitSet; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, - query::{Access, FilteredAccessSet}, - schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, - system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, + query::FilteredAccessSet, + schedule::{ConditionWithAccess, InternedSystemSet, NodeId, SystemTypeSet, SystemWithAccess}, + system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; @@ -75,9 +75,9 @@ pub struct SystemSchedule { /// List of system node ids. pub(super) system_ids: Vec, /// Indexed by system node id. - pub(super) systems: Vec, + pub(super) systems: Vec, /// Indexed by system node id. - pub(super) system_conditions: Vec>, + pub(super) system_conditions: Vec>, /// Indexed by system node id. /// Number of systems that the system immediately depends on. #[cfg_attr( @@ -98,7 +98,7 @@ pub struct SystemSchedule { /// List of system set node ids. pub(super) set_ids: Vec, /// Indexed by system set node id. - pub(super) set_conditions: Vec>, + pub(super) set_conditions: Vec>, /// Indexed by system set node id. /// List of systems that are in sets that have conditions. /// @@ -159,44 +159,13 @@ impl System for ApplyDeferred { type In = (); type Out = Result<()>; - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("bevy_ecs::apply_deferred") + fn name(&self) -> DebugName { + DebugName::borrowed("bevy_ecs::apply_deferred") } - fn component_access(&self) -> &Access { - // This system accesses no components. - const { &Access::new() } - } - - fn component_access_set(&self) -> &FilteredAccessSet { - const { &FilteredAccessSet::new() } - } - - fn archetype_component_access(&self) -> &Access { - // This system accesses no archetype components. - const { &Access::new() } - } - - fn is_send(&self) -> bool { - // Although this system itself does nothing on its own, the system - // executor uses it to apply deferred commands. Commands must be allowed - // to access non-send resources, so this system must be non-send for - // scheduling purposes. - false - } - - fn is_exclusive(&self) -> bool { - // This system is labeled exclusive because it is used by the system - // executor to find places where deferred commands should be applied, - // and commands can only be applied with exclusive access to the world. - true - } - - fn has_deferred(&self) -> bool { - // This system itself doesn't have any commands to apply, but when it - // is pulled from the schedule to be ran, the executor will apply - // deferred commands from other systems. - false + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } unsafe fn run_unsafe( @@ -209,6 +178,10 @@ impl System for ApplyDeferred { Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) {} + fn run(&mut self, _input: SystemIn<'_, Self>, _world: &mut World) -> Self::Out { // This system does nothing on its own. The executor will apply deferred // commands from other systems instead of running this system. @@ -228,11 +201,11 @@ impl System for ApplyDeferred { Ok(()) } - fn initialize(&mut self, _world: &mut World) {} + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet { + FilteredAccessSet::new() + } - fn update_archetype_component_access(&mut self, _world: UnsafeWorldCell) {} - - fn check_change_tick(&mut self, _change_tick: Tick) {} + fn check_change_tick(&mut self, _check: CheckChangeTicks) {} fn default_system_sets(&self) -> Vec { vec![SystemTypeSet::::new().intern()] @@ -370,7 +343,7 @@ mod tests { #[expect(clippy::print_stdout, reason = "std and println are allowed in tests")] fn single_and_populated_skipped_and_run() { for executor in EXECUTORS { - std::println!("Testing executor: {:?}", executor); + std::println!("Testing executor: {executor:?}"); let mut world = World::new(); world.init_resource::(); diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index b0757cc031..b6036ee76b 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -1,7 +1,7 @@ use alloc::{boxed::Box, vec::Vec}; +use bevy_platform::cell::SyncUnsafeCell; use bevy_platform::sync::Arc; use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor}; -use bevy_utils::syncunsafecell::SyncUnsafeCell; use concurrent_queue::ConcurrentQueue; use core::{any::Any, panic::AssertUnwindSafe}; use fixedbitset::FixedBitSet; @@ -13,26 +13,31 @@ use std::sync::{Mutex, MutexGuard}; use tracing::{info_span, Span}; use crate::{ - error::{default_error_handler, BevyError, ErrorContext, Result}, + error::{ErrorContext, ErrorHandler, Result}, prelude::Resource, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + SystemWithAccess, + }, system::ScheduleSystem, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; /// Borrowed data used by the [`MultiThreadedExecutor`]. struct Environment<'env, 'sys> { executor: &'env MultiThreadedExecutor, - systems: &'sys [SyncUnsafeCell], + systems: &'sys [SyncUnsafeCell], conditions: SyncUnsafeCell>, world_cell: UnsafeWorldCell<'env>, } struct Conditions<'a> { - system_conditions: &'a mut [Vec], - set_conditions: &'a mut [Vec], + system_conditions: &'a mut [Vec], + set_conditions: &'a mut [Vec], sets_with_conditions_of_systems: &'a [FixedBitSet], systems_in_sets_with_conditions: &'a [FixedBitSet], } @@ -134,7 +139,7 @@ pub struct ExecutorState { struct Context<'scope, 'env, 'sys> { environment: &'env Environment<'env, 'sys>, scope: &'scope Scope<'scope, 'env, ()>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, } impl Default for MultiThreadedExecutor { @@ -170,8 +175,8 @@ impl SystemExecutor for MultiThreadedExecutor { conflicting_systems: FixedBitSet::with_capacity(sys_count), condition_conflicting_systems: FixedBitSet::with_capacity(sys_count), dependents: schedule.system_dependents[index].clone(), - is_send: schedule.systems[index].is_send(), - is_exclusive: schedule.systems[index].is_exclusive(), + is_send: schedule.systems[index].system.is_send(), + is_exclusive: schedule.systems[index].system.is_exclusive(), }); if schedule.system_dependencies[index] == 0 { self.starting_systems.insert(index); @@ -185,10 +190,7 @@ impl SystemExecutor for MultiThreadedExecutor { let system1 = &schedule.systems[index1]; for index2 in 0..index1 { let system2 = &schedule.systems[index2]; - if !system2 - .component_access_set() - .is_compatible(system1.component_access_set()) - { + if !system2.access.is_compatible(&system1.access) { state.system_task_metadata[index1] .conflicting_systems .insert(index2); @@ -200,11 +202,10 @@ impl SystemExecutor for MultiThreadedExecutor { for index2 in 0..sys_count { let system2 = &schedule.systems[index2]; - if schedule.system_conditions[index1].iter().any(|condition| { - !system2 - .component_access_set() - .is_compatible(condition.component_access_set()) - }) { + if schedule.system_conditions[index1] + .iter() + .any(|condition| !system2.access.is_compatible(&condition.access)) + { state.system_task_metadata[index1] .condition_conflicting_systems .insert(index2); @@ -218,11 +219,10 @@ impl SystemExecutor for MultiThreadedExecutor { let mut conflicting_systems = FixedBitSet::with_capacity(sys_count); for sys_index in 0..sys_count { let system = &schedule.systems[sys_index]; - if schedule.set_conditions[set_idx].iter().any(|condition| { - !system - .component_access_set() - .is_compatible(condition.component_access_set()) - }) { + if schedule.set_conditions[set_idx] + .iter() + .any(|condition| !system.access.is_compatible(&condition.access)) + { conflicting_systems.insert(sys_index); } } @@ -240,7 +240,7 @@ impl SystemExecutor for MultiThreadedExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { let state = self.state.get_mut().unwrap(); // reset counts @@ -342,7 +342,7 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> { #[cfg(feature = "std")] #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); } // set the payload to propagate the error { @@ -443,6 +443,14 @@ impl ExecutorState { return; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !context + .environment + .world_cell + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + // can't borrow since loop mutably borrows `self` let mut ready_systems = core::mem::take(&mut self.ready_systems_copy); @@ -458,14 +466,15 @@ impl ExecutorState { debug_assert!(!self.running_systems.contains(system_index)); // SAFETY: Caller assured that these systems are not running. // Therefore, no other reference to this system exists and there is no aliasing. - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = + &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; - if !self.can_run( - system_index, - system, - conditions, - context.environment.world_cell, - ) { + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + + if !self.can_run(system_index, conditions) { // NOTE: exclusive systems with ambiguities are susceptible to // being significantly displaced here (compared to single-threaded order) // if systems after them in topological order can run @@ -476,7 +485,6 @@ impl ExecutorState { self.ready_systems.remove(system_index); // SAFETY: `can_run` returned true, which means that: - // - It must have called `update_archetype_component_access` for each run condition. // - There can be no systems running whose accesses would conflict with any conditions. if unsafe { !self.should_run( @@ -484,6 +492,7 @@ impl ExecutorState { system, conditions, context.environment.world_cell, + context.error_handler, ) } { self.skip_system_and_signal_dependents(system_index); @@ -509,7 +518,6 @@ impl ExecutorState { // - Caller ensured no other reference to this system exists. // - `system_task_metadata[system_index].is_exclusive` is `false`, // so `System::is_exclusive` returned `false` when we called it. - // - `can_run` has been called, which calls `update_archetype_component_access` with this system. // - `can_run` returned true, so no systems with conflicting world access are running. unsafe { self.spawn_system_task(context, system_index); @@ -521,13 +529,7 @@ impl ExecutorState { self.ready_systems_copy = ready_systems; } - fn can_run( - &mut self, - system_index: usize, - system: &mut ScheduleSystem, - conditions: &mut Conditions, - world: UnsafeWorldCell, - ) -> bool { + fn can_run(&mut self, system_index: usize, conditions: &mut Conditions) -> bool { let system_meta = &self.system_task_metadata[system_index]; if system_meta.is_exclusive && self.num_running_systems > 0 { return false; @@ -541,17 +543,11 @@ impl ExecutorState { for set_idx in conditions.sets_with_conditions_of_systems[system_index] .difference(&self.evaluated_sets) { - for condition in &mut conditions.set_conditions[set_idx] { - condition.update_archetype_component_access(world); - } if !self.set_condition_conflicting_systems[set_idx].is_disjoint(&self.running_systems) { return false; } } - for condition in &mut conditions.system_conditions[system_index] { - condition.update_archetype_component_access(world); - } if !system_meta .condition_conflicting_systems .is_disjoint(&self.running_systems) @@ -559,14 +555,12 @@ impl ExecutorState { return false; } - if !self.skipped_systems.contains(system_index) { - system.update_archetype_component_access(world); - if !system_meta + if !self.skipped_systems.contains(system_index) + && !system_meta .conflicting_systems .is_disjoint(&self.running_systems) - { - return false; - } + { + return false; } true @@ -576,17 +570,15 @@ impl ExecutorState { /// * `world` must have permission to read any world data required by /// the system's conditions: this includes conditions for the system /// itself, and conditions for any of the system's sets. - /// * `update_archetype_component` must have been called with `world` - /// for the system as well as system and system set's run conditions. unsafe fn should_run( &mut self, system_index: usize, system: &mut ScheduleSystem, conditions: &mut Conditions, world: UnsafeWorldCell, + error_handler: ErrorHandler, ) -> bool { let mut should_run = !self.skipped_systems.contains(system_index); - let error_handler = default_error_handler(); for set_idx in conditions.sets_with_conditions_of_systems[system_index].ones() { if self.evaluated_sets.contains(set_idx) { @@ -597,9 +589,12 @@ impl ExecutorState { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the conditions. - // - `update_archetype_component_access` has been called for each run condition. let set_conditions_met = unsafe { - evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world) + evaluate_and_fold_conditions( + &mut conditions.set_conditions[set_idx], + world, + error_handler, + ) }; if !set_conditions_met { @@ -615,9 +610,12 @@ impl ExecutorState { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the conditions. - // - `update_archetype_component_access` has been called for each run condition. let system_conditions_met = unsafe { - evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world) + evaluate_and_fold_conditions( + &mut conditions.system_conditions[system_index], + world, + error_handler, + ) }; if !system_conditions_met { @@ -630,7 +628,6 @@ impl ExecutorState { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the system. - // - `update_archetype_component_access` has been called for system. let valid_params = match unsafe { system.validate_param_unsafe(world) } { Ok(()) => true, Err(e) => { @@ -661,11 +658,9 @@ impl ExecutorState { /// - `is_exclusive` must have returned `false` for the specified system. /// - `world` must have permission to access the world data /// used by the specified system. - /// - `update_archetype_component_access` must have been called with `world` - /// on the system associated with `system_index`. unsafe fn spawn_system_task(&mut self, context: &Context, system_index: usize) { // SAFETY: this system is not running, no other reference exists - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; // Move the full context object into the new future. let context = *context; @@ -677,7 +672,6 @@ impl ExecutorState { // - The caller ensures that we have permission to // access the world data used by the system. // - `is_exclusive` returned false - // - `update_archetype_component_access` has been called. unsafe { if let Err(err) = __rust_begin_short_backtrace::run_unsafe( system, @@ -708,7 +702,7 @@ impl ExecutorState { /// Caller must ensure no systems are currently borrowed. unsafe fn spawn_exclusive_system_task(&mut self, context: &Context, system_index: usize) { // SAFETY: this system is not running, no other reference exists - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; // Move the full context object into the new future. let context = *context; @@ -790,12 +784,12 @@ impl ExecutorState { fn apply_deferred( unapplied_systems: &FixedBitSet, - systems: &[SyncUnsafeCell], + systems: &[SyncUnsafeCell], world: &mut World, ) -> Result<(), Box> { for system_index in unapplied_systems.ones() { // SAFETY: none of these systems are running, no other references exist - let system = unsafe { &mut *systems[system_index].get() }; + let system = &mut unsafe { &mut *systems[system_index].get() }.system; let res = std::panic::catch_unwind(AssertUnwindSafe(|| { system.apply_deferred(world); })); @@ -805,7 +799,7 @@ fn apply_deferred( { eprintln!( "Encountered a panic when applying buffers for system `{}`!", - &*system.name() + system.name() ); } return Err(payload); @@ -817,25 +811,21 @@ fn apply_deferred( /// # Safety /// - `world` must have permission to read any world data /// required by `conditions`. -/// - `update_archetype_component_access` must have been called -/// with `world` for each condition in `conditions`. unsafe fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: UnsafeWorldCell, + error_handler: ErrorHandler, ) -> bool { - let error_handler = default_error_handler(); - #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the condition. - // - `update_archetype_component_access` has been called for condition. match unsafe { condition.validate_param_unsafe(world) } { Ok(()) => (), Err(e) => { @@ -854,7 +844,6 @@ unsafe fn evaluate_and_fold_conditions( // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the condition. - // - `update_archetype_component_access` has been called for condition. unsafe { __rust_begin_short_backtrace::readonly_run_unsafe(&mut **condition, world) } }) .fold(true, |acc, res| acc && res) diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 701a8d8f06..f0d655eab8 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -10,12 +10,15 @@ use tracing::info_span; use std::eprintln; use crate::{ - error::{default_error_handler, BevyError, ErrorContext}, + error::{ErrorContext, ErrorHandler}, schedule::{ - executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, + executor::is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, + SystemSchedule, }, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -50,7 +53,7 @@ impl SystemExecutor for SimpleExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { // If stepping is enabled, make sure we skip those systems that should // not be run. @@ -60,11 +63,17 @@ impl SystemExecutor for SimpleExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] - let name = schedule.systems[system_index].name(); + let name = schedule.systems[system_index].system.name(); #[cfg(feature = "trace")] - let should_run_span = info_span!("check_conditions", name = &*name).entered(); + let should_run_span = info_span!("check_conditions", name = name.as_string()).entered(); let mut should_run = !self.completed_systems.contains(system_index); for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() { @@ -73,8 +82,11 @@ impl SystemExecutor for SimpleExecutor { } // evaluate system set's conditions - let set_conditions_met = - evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + let set_conditions_met = evaluate_and_fold_conditions( + &mut schedule.set_conditions[set_idx], + world, + error_handler, + ); if !set_conditions_met { self.completed_systems @@ -86,12 +98,15 @@ impl SystemExecutor for SimpleExecutor { } // evaluate system's conditions - let system_conditions_met = - evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + let system_conditions_met = evaluate_and_fold_conditions( + &mut schedule.system_conditions[system_index], + world, + error_handler, + ); should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; if should_run { let valid_params = match system.validate_param(world) { Ok(()) => true, @@ -114,6 +129,11 @@ impl SystemExecutor for SimpleExecutor { #[cfg(feature = "trace")] should_run_span.exit(); + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + // system has either been skipped or will run self.completed_systems.insert(system_index); @@ -141,7 +161,7 @@ impl SystemExecutor for SimpleExecutor { #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); std::panic::resume_unwind(payload); } } @@ -175,8 +195,16 @@ impl SimpleExecutor { since = "0.17.0", note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." )] -fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - let error_handler = default_error_handler(); +fn evaluate_and_fold_conditions( + conditions: &mut [ConditionWithAccess], + world: &mut World, + error_handler: ErrorHandler, +) -> bool { + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); #[expect( clippy::unnecessary_fold, @@ -184,7 +212,7 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { match condition.validate_param(world) { Ok(()) => (), Err(e) => { @@ -200,6 +228,10 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W return false; } } + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + condition.refresh_hotpatch(); + } __rust_begin_short_backtrace::readonly_run(&mut **condition, world) }) .fold(true, |acc, res| acc && res) diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 82e9e354a8..21b8d2289d 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -8,10 +8,14 @@ use tracing::info_span; use std::eprintln; use crate::{ - error::{default_error_handler, BevyError, ErrorContext}, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + error::{ErrorContext, ErrorHandler}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + }, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -50,7 +54,7 @@ impl SystemExecutor for SingleThreadedExecutor { schedule: &mut SystemSchedule, world: &mut World, _skip_systems: Option<&FixedBitSet>, - error_handler: fn(BevyError, ErrorContext), + error_handler: ErrorHandler, ) { // If stepping is enabled, make sure we skip those systems that should // not be run. @@ -60,11 +64,17 @@ impl SystemExecutor for SingleThreadedExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] - let name = schedule.systems[system_index].name(); + let name = schedule.systems[system_index].system.name(); #[cfg(feature = "trace")] - let should_run_span = info_span!("check_conditions", name = &*name).entered(); + let should_run_span = info_span!("check_conditions", name = name.as_string()).entered(); let mut should_run = !self.completed_systems.contains(system_index); for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() { @@ -73,8 +83,11 @@ impl SystemExecutor for SingleThreadedExecutor { } // evaluate system set's conditions - let set_conditions_met = - evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + let set_conditions_met = evaluate_and_fold_conditions( + &mut schedule.set_conditions[set_idx], + world, + error_handler, + ); if !set_conditions_met { self.completed_systems @@ -86,12 +99,15 @@ impl SystemExecutor for SingleThreadedExecutor { } // evaluate system's conditions - let system_conditions_met = - evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + let system_conditions_met = evaluate_and_fold_conditions( + &mut schedule.system_conditions[system_index], + world, + error_handler, + ); should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; if should_run { let valid_params = match system.validate_param(world) { Ok(()) => true, @@ -115,6 +131,11 @@ impl SystemExecutor for SingleThreadedExecutor { #[cfg(feature = "trace")] should_run_span.exit(); + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + // system has either been skipped or will run self.completed_systems.insert(system_index); @@ -145,7 +166,7 @@ impl SystemExecutor for SingleThreadedExecutor { #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); std::panic::resume_unwind(payload); } } @@ -185,7 +206,7 @@ impl SingleThreadedExecutor { fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) { for system_index in self.unapplied_systems.ones() { - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; system.apply_deferred(world); } @@ -193,8 +214,16 @@ impl SingleThreadedExecutor { } } -fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - let error_handler: fn(BevyError, ErrorContext) = default_error_handler(); +fn evaluate_and_fold_conditions( + conditions: &mut [ConditionWithAccess], + world: &mut World, + error_handler: ErrorHandler, +) -> bool { + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); #[expect( clippy::unnecessary_fold, @@ -202,7 +231,7 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { match condition.validate_param(world) { Ok(()) => (), Err(e) => { @@ -218,6 +247,10 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W return false; } } + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + condition.refresh_hotpatch(); + } __rust_begin_short_backtrace::readonly_run(&mut **condition, world) }) .fold(true, |acc, res| acc && res) diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 81912d2f72..12f58a7cd3 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -26,9 +26,12 @@ pub mod passes { #[cfg(test)] mod tests { use super::*; - use alloc::{string::ToString, vec, vec::Vec}; + #[cfg(feature = "trace")] + use alloc::string::ToString; + use alloc::{vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; + use crate::error::BevyError; pub use crate::{ prelude::World, resource::Resource, @@ -49,10 +52,10 @@ mod tests { struct SystemOrder(Vec); #[derive(Resource, Default)] - struct RunConditionBool(pub bool); + struct RunConditionBool(bool); #[derive(Resource, Default)] - struct Counter(pub AtomicU32); + struct Counter(AtomicU32); fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) { move |world| world.resource_mut::().0.push(tag) @@ -252,12 +255,13 @@ mod tests { } mod conditions { + use crate::change_detection::DetectChanges; use super::*; #[test] - fn system_with_condition() { + fn system_with_condition_bool() { let mut world = World::default(); let mut schedule = Schedule::default(); @@ -276,6 +280,47 @@ mod tests { assert_eq!(world.resource::().0, vec![0]); } + #[test] + fn system_with_condition_result_unit() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems( + make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(1).run_if(|| Ok(()))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![1]); + } + + #[test] + fn system_with_condition_result_bool() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems(( + make_function_system(0).run_if(|| Err::(core::fmt::Error.into())), + make_function_system(1).run_if(|| Ok(false)), + )); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(2).run_if(|| Ok(true))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![2]); + } + #[test] fn systems_with_distributive_condition() { let mut world = World::default(); @@ -727,6 +772,7 @@ mod tests { } mod system_ambiguity { + #[cfg(feature = "trace")] use alloc::collections::BTreeSet; use super::*; @@ -741,8 +787,7 @@ mod tests { #[derive(Component)] struct B; - // An event type - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct E; #[derive(Resource, Component)] @@ -874,7 +919,6 @@ mod tests { } #[test] - #[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"] fn filtered_components() { let mut world = World::new(); world.spawn(A); @@ -1069,6 +1113,7 @@ mod tests { // Tests that the correct ambiguities were reported in the correct order. #[test] + #[cfg(feature = "trace")] fn correct_ambiguities() { fn system_a(_res: ResMut) {} fn system_b(_res: ResMut) {} @@ -1142,6 +1187,7 @@ mod tests { // Test that anonymous set names work properly // Related issue https://github.com/bevyengine/bevy/issues/9641 #[test] + #[cfg(feature = "trace")] fn anonymous_set_name() { let mut schedule = Schedule::new(TestSchedule); schedule.add_systems((resmut_system, resmut_system).run_if(|| true)); diff --git a/crates/bevy_ecs/src/schedule/pass.rs b/crates/bevy_ecs/src/schedule/pass.rs index 20680e04e0..a602877d65 100644 --- a/crates/bevy_ecs/src/schedule/pass.rs +++ b/crates/bevy_ecs/src/schedule/pass.rs @@ -51,6 +51,7 @@ pub(super) trait ScheduleBuildPassObj: Send + Sync + Debug { ); fn add_dependency(&mut self, from: NodeId, to: NodeId, all_options: &TypeIdMap>); } + impl ScheduleBuildPassObj for T { fn build( &mut self, diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index a754f1e1d4..4455aba2fe 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -2,7 +2,6 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17344." )] -use alloc::borrow::Cow; use alloc::{ boxed::Box, collections::{BTreeMap, BTreeSet}, @@ -12,12 +11,11 @@ use alloc::{ vec::Vec, }; use bevy_platform::collections::{HashMap, HashSet}; -use bevy_utils::{default, TypeIdMap}; +use bevy_utils::{default, prelude::DebugName, TypeIdMap}; use core::{ any::{Any, TypeId}, fmt::{Debug, Write}, }; -use disqualified::ShortName; use fixedbitset::FixedBitSet; use log::{error, info, warn}; use pass::ScheduleBuildPassObj; @@ -25,10 +23,11 @@ use thiserror::Error; #[cfg(feature = "trace")] use tracing::info_span; +use crate::component::CheckChangeTicks; use crate::{ - component::{ComponentId, Components, Tick}, - error::default_error_handler, + component::{ComponentId, Components}, prelude::Component, + query::FilteredAccessSet, resource::Resource, schedule::*, system::ScheduleSystem, @@ -112,7 +111,7 @@ impl Schedules { /// Iterates the change ticks of all systems in all stored schedules and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { #[cfg(feature = "trace")] let _all_span = info_span!("check stored schedule ticks").entered(); #[cfg_attr( @@ -127,7 +126,7 @@ impl Schedules { let name = format!("{label:?}"); #[cfg(feature = "trace")] let _one_span = info_span!("check schedule ticks", name = &name).entered(); - schedule.check_change_ticks(change_tick); + schedule.check_change_ticks(check); } } @@ -167,7 +166,7 @@ impl Schedules { writeln!(message, "{}", components.get_name(*id).unwrap()).unwrap(); } - info!("{}", message); + info!("{message}"); } /// Adds one or more systems to the [`Schedule`] matching the provided [`ScheduleLabel`]. @@ -236,6 +235,7 @@ pub enum Chain { /// will be added between the successive elements. Chained(TypeIdMap>), } + impl Chain { /// Specify that the systems must be chained. pub fn set_chained(&mut self) { @@ -258,8 +258,16 @@ impl Chain { /// A collection of systems, and the metadata and executor needed to run them /// in a certain order under certain conditions. /// +/// # Schedule labels +/// +/// Each schedule has a [`ScheduleLabel`] value. This value is used to uniquely identify the +/// schedule when added to a [`World`]’s [`Schedules`], and may be used to specify which schedule +/// a system should be added to. +/// /// # Example +/// /// Here is an example of a `Schedule` running a "Hello world" system: +/// /// ``` /// # use bevy_ecs::prelude::*; /// fn hello_world() { println!("Hello world!") } @@ -274,6 +282,7 @@ impl Chain { /// ``` /// /// A schedule can also run several systems in an ordered way: +/// /// ``` /// # use bevy_ecs::prelude::*; /// fn system_one() { println!("System 1 works!") } @@ -292,6 +301,32 @@ impl Chain { /// schedule.run(&mut world); /// } /// ``` +/// +/// Schedules are often inserted into a [`World`] and identified by their [`ScheduleLabel`] only: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// use bevy_ecs::schedule::ScheduleLabel; +/// +/// // Declare a new schedule label. +/// #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] +/// struct Update; +/// +/// // This system shall be part of the schedule. +/// fn an_update_system() { +/// println!("Hello world!"); +/// } +/// +/// fn main() { +/// let mut world = World::new(); +/// +/// // Add a system to the schedule with that label (creating it automatically). +/// world.get_resource_or_init::().add_systems(Update, an_update_system); +/// +/// // Run the schedule, and therefore run the system. +/// world.run_schedule(Update); +/// } +/// ``` pub struct Schedule { label: InternedScheduleLabel, graph: ScheduleGraph, @@ -328,7 +363,8 @@ impl Schedule { this } - /// Get the `InternedScheduleLabel` for this `Schedule`. + /// Returns the [`InternedScheduleLabel`] for this `Schedule`, + /// corresponding to the [`ScheduleLabel`] this schedule was created with. pub fn label(&self) -> InternedScheduleLabel { self.label } @@ -395,9 +431,21 @@ impl Schedule { } /// Changes miscellaneous build settings. + /// + /// If [`settings.auto_insert_apply_deferred`][ScheduleBuildSettings::auto_insert_apply_deferred] + /// is `false`, this clears `*_ignore_deferred` edge settings configured so far. + /// + /// Generally this method should be used before adding systems or set configurations to the schedule, + /// not after. pub fn set_build_settings(&mut self, settings: ScheduleBuildSettings) -> &mut Self { if settings.auto_insert_apply_deferred { - self.add_build_pass(passes::AutoInsertApplyDeferredPass::default()); + if !self + .graph + .passes + .contains_key(&TypeId::of::()) + { + self.add_build_pass(passes::AutoInsertApplyDeferredPass::default()); + } } else { self.remove_build_pass::(); } @@ -442,7 +490,7 @@ impl Schedule { self.initialize(world) .unwrap_or_else(|e| panic!("Error when initializing schedule {:?}: {e}", self.label)); - let error_handler = default_error_handler(); + let error_handler = world.default_error_handler(); #[cfg(not(feature = "bevy_debug_stepping"))] self.executor @@ -511,22 +559,22 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - for system in &mut self.executable.systems { + pub fn check_change_ticks(&mut self, check: CheckChangeTicks) { + for SystemWithAccess { system, .. } in &mut self.executable.systems { if !is_apply_deferred(system) { - system.check_change_tick(change_tick); + system.check_change_tick(check); } } for conditions in &mut self.executable.system_conditions { for system in conditions { - system.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } for conditions in &mut self.executable.set_conditions { for system in conditions { - system.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } } @@ -540,7 +588,7 @@ impl Schedule { /// This is used in rendering to extract data from the main world, storing the data in system buffers, /// before applying their buffers in a different world. pub fn apply_deferred(&mut self, world: &mut World) { - for system in &mut self.executable.systems { + for SystemWithAccess { system, .. } in &mut self.executable.systems { system.apply_deferred(world); } } @@ -562,7 +610,7 @@ impl Schedule { .system_ids .iter() .zip(&self.executable.systems) - .map(|(node_id, system)| (*node_id, system)); + .map(|(node_id, system)| (*node_id, &system.system)); Ok(iter) } @@ -630,26 +678,66 @@ impl SystemSetNode { } } -/// A [`ScheduleSystem`] stored in a [`ScheduleGraph`]. +/// A [`SystemWithAccess`] stored in a [`ScheduleGraph`]. pub struct SystemNode { - inner: Option, + inner: Option, +} + +/// A [`ScheduleSystem`] stored alongside the access returned from [`System::initialize`](crate::system::System::initialize). +pub struct SystemWithAccess { + /// The system itself. + pub system: ScheduleSystem, + /// The access returned by [`System::initialize`](crate::system::System::initialize). + /// This will be empty if the system has not been initialized yet. + pub access: FilteredAccessSet, +} + +impl SystemWithAccess { + /// Constructs a new [`SystemWithAccess`] from a [`ScheduleSystem`]. + /// The `access` will initially be empty. + pub fn new(system: ScheduleSystem) -> Self { + Self { + system, + access: FilteredAccessSet::new(), + } + } +} + +/// A [`BoxedCondition`] stored alongside the access returned from [`System::initialize`](crate::system::System::initialize). +pub struct ConditionWithAccess { + /// The condition itself. + pub condition: BoxedCondition, + /// The access returned by [`System::initialize`](crate::system::System::initialize). + /// This will be empty if the system has not been initialized yet. + pub access: FilteredAccessSet, +} + +impl ConditionWithAccess { + /// Constructs a new [`ConditionWithAccess`] from a [`BoxedCondition`]. + /// The `access` will initially be empty. + pub const fn new(condition: BoxedCondition) -> Self { + Self { + condition, + access: FilteredAccessSet::new(), + } + } } impl SystemNode { /// Create a new [`SystemNode`] pub fn new(system: ScheduleSystem) -> Self { Self { - inner: Some(system), + inner: Some(SystemWithAccess::new(system)), } } - /// Obtain a reference to the [`ScheduleSystem`] represented by this node. - pub fn get(&self) -> Option<&ScheduleSystem> { + /// Obtain a reference to the [`SystemWithAccess`] represented by this node. + pub fn get(&self) -> Option<&SystemWithAccess> { self.inner.as_ref() } - /// Obtain a mutable reference to the [`ScheduleSystem`] represented by this node. - pub fn get_mut(&mut self) -> Option<&mut ScheduleSystem> { + /// Obtain a mutable reference to the [`SystemWithAccess`] represented by this node. + pub fn get_mut(&mut self) -> Option<&mut SystemWithAccess> { self.inner.as_mut() } } @@ -663,11 +751,11 @@ pub struct ScheduleGraph { /// List of systems in the schedule pub systems: Vec, /// List of conditions for each system, in the same order as `systems` - pub system_conditions: Vec>, + pub system_conditions: Vec>, /// List of system sets in the schedule system_sets: Vec, /// List of conditions for each system set, in the same order as `system_sets` - system_set_conditions: Vec>, + system_set_conditions: Vec>, /// Map from system set to node id system_set_ids: HashMap, /// Systems that have not been initialized yet; for system sets, we store the index of the first uninitialized condition @@ -718,6 +806,7 @@ impl ScheduleGraph { self.systems .get(id.index()) .and_then(|system| system.inner.as_ref()) + .map(|system| &system.system) } /// Returns `true` if the given system set is part of the graph. Otherwise, returns `false`. @@ -754,7 +843,7 @@ impl ScheduleGraph { } /// Returns the conditions for the set at the given [`NodeId`], if it exists. - pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[BoxedCondition]> { + pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[ConditionWithAccess]> { if !id.is_set() { return None; } @@ -767,27 +856,31 @@ impl ScheduleGraph { /// /// Panics if it doesn't exist. #[track_caller] - pub fn set_conditions_at(&self, id: NodeId) -> &[BoxedCondition] { + pub fn set_conditions_at(&self, id: NodeId) -> &[ConditionWithAccess] { self.get_set_conditions_at(id) .ok_or_else(|| format!("set with id {id:?} does not exist in this Schedule")) .unwrap() } /// Returns an iterator over all systems in this schedule, along with the conditions for each system. - pub fn systems(&self) -> impl Iterator { + pub fn systems( + &self, + ) -> impl Iterator { self.systems .iter() .zip(self.system_conditions.iter()) .enumerate() .filter_map(|(i, (system_node, condition))| { - let system = system_node.inner.as_ref()?; + let system = &system_node.inner.as_ref()?.system; Some((NodeId::System(i), system, condition.as_slice())) }) } /// Returns an iterator over all system sets in this schedule, along with the conditions for each /// system set. - pub fn system_sets(&self) -> impl Iterator { + pub fn system_sets( + &self, + ) -> impl Iterator { self.system_set_ids.iter().map(|(_, &node_id)| { let set_node = &self.system_sets[node_id.index()]; let set = &*set_node.inner; @@ -966,7 +1059,13 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) self.uninit.push((id, 0)); self.systems.push(SystemNode::new(config.node)); - self.system_conditions.push(config.conditions); + self.system_conditions.push( + config + .conditions + .into_iter() + .map(ConditionWithAccess::new) + .collect(), + ); Ok(id) } @@ -984,7 +1083,7 @@ impl ScheduleGraph { let ScheduleConfig { node: set, metadata, - mut conditions, + conditions, } = set; let id = match self.system_set_ids.get(&set) { @@ -998,7 +1097,7 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) let system_set_conditions = &mut self.system_set_conditions[id.index()]; self.uninit.push((id, system_set_conditions.len())); - system_set_conditions.append(&mut conditions); + system_set_conditions.extend(conditions.into_iter().map(ConditionWithAccess::new)); Ok(id) } @@ -1150,14 +1249,15 @@ impl ScheduleGraph { for (id, i) in self.uninit.drain(..) { match id { NodeId::System(index) => { - self.systems[index].get_mut().unwrap().initialize(world); + let system = self.systems[index].get_mut().unwrap(); + system.access = system.system.initialize(world); for condition in &mut self.system_conditions[index] { - condition.initialize(world); + condition.access = condition.condition.initialize(world); } } NodeId::Set(index) => { for condition in self.system_set_conditions[index].iter_mut().skip(i) { - condition.initialize(world); + condition.access = condition.condition.initialize(world); } } } @@ -1368,11 +1468,11 @@ impl ScheduleGraph { let system_a = self.systems[a.index()].get().unwrap(); let system_b = self.systems[b.index()].get().unwrap(); - if system_a.is_exclusive() || system_b.is_exclusive() { + if system_a.system.is_exclusive() || system_b.system.is_exclusive() { conflicting_systems.push((a, b, Vec::new())); } else { - let access_a = system_a.component_access(); - let access_b = system_b.component_access(); + let access_a = &system_a.access; + let access_b = &system_b.access; if !access_a.is_compatible(access_b) { match access_a.get_conflicts(access_b) { AccessConflicts::Individual(conflicts) => { @@ -1593,9 +1693,14 @@ impl ScheduleGraph { #[inline] fn get_node_name_inner(&self, id: &NodeId, report_sets: bool) -> String { - let name = match id { + match id { NodeId::System(_) => { - let name = self.systems[id.index()].get().unwrap().name().to_string(); + let name = self.systems[id.index()].get().unwrap().system.name(); + let name = if self.settings.use_shortnames { + name.shortname().to_string() + } else { + name.to_string() + }; if report_sets { let sets = self.names_of_sets_containing_node(id); if sets.is_empty() { @@ -1617,11 +1722,6 @@ impl ScheduleGraph { set.name() } } - }; - if self.settings.use_shortnames { - ShortName(&name).to_string() - } else { - name } } @@ -1660,10 +1760,7 @@ impl ScheduleGraph { match self.settings.hierarchy_detection { LogLevel::Ignore => unreachable!(), LogLevel::Warn => { - error!( - "Schedule {schedule_label:?} has redundant edges:\n {}", - message - ); + error!("Schedule {schedule_label:?} has redundant edges:\n {message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), @@ -1865,7 +1962,7 @@ impl ScheduleGraph { match self.settings.ambiguity_detection { LogLevel::Ignore => Ok(()), LogLevel::Warn => { - warn!("Schedule {schedule_label:?} has ambiguities.\n{}", message); + warn!("Schedule {schedule_label:?} has ambiguities.\n{message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), @@ -1904,7 +2001,7 @@ impl ScheduleGraph { &'a self, ambiguities: &'a [(NodeId, NodeId, Vec)], components: &'a Components, - ) -> impl Iterator>)> + 'a { + ) -> impl Iterator)> + 'a { ambiguities .iter() .map(move |(system_a, system_b, conflicts)| { @@ -2061,6 +2158,7 @@ mod tests { use bevy_ecs_macros::ScheduleLabel; use crate::{ + error::{ignore, panic, DefaultErrorHandler, Result}, prelude::{ApplyDeferred, Res, Resource}, schedule::{ tests::ResMut, IntoScheduleConfigs, Schedule, ScheduleBuildSettings, SystemSet, @@ -2077,6 +2175,46 @@ mod tests { #[derive(Resource)] struct Resource2; + #[test] + fn unchanged_auto_insert_apply_deferred_has_no_effect() { + use alloc::{vec, vec::Vec}; + + #[derive(PartialEq, Debug)] + enum Entry { + System(usize), + SyncPoint(usize), + } + + #[derive(Resource, Default)] + struct Log(Vec); + + fn system(mut res: ResMut, mut commands: Commands) { + res.0.push(Entry::System(N)); + commands + .queue(|world: &mut World| world.resource_mut::().0.push(Entry::SyncPoint(N))); + } + + let mut world = World::default(); + world.init_resource::(); + let mut schedule = Schedule::default(); + schedule.add_systems((system::<1>, system::<2>).chain_ignore_deferred()); + schedule.set_build_settings(ScheduleBuildSettings { + auto_insert_apply_deferred: true, + ..Default::default() + }); + schedule.run(&mut world); + let actual = world.remove_resource::().unwrap().0; + + let expected = vec![ + Entry::System(1), + Entry::System(2), + Entry::SyncPoint(1), + Entry::SyncPoint(2), + ]; + + assert_eq!(actual, expected); + } + // regression test for https://github.com/bevyengine/bevy/issues/9114 #[test] fn ambiguous_with_not_breaking_run_conditions() { @@ -2810,4 +2948,32 @@ mod tests { .expect("CheckSystemRan Resource Should Exist"); assert_eq!(value.0, 2); } + + #[test] + fn test_default_error_handler() { + #[derive(Resource, Default)] + struct Ran(bool); + + fn system(mut ran: ResMut) -> Result { + ran.0 = true; + Err("I failed!".into()) + } + + // Test that the default error handler is used + let mut world = World::default(); + world.init_resource::(); + world.insert_resource(DefaultErrorHandler(ignore)); + let mut schedule = Schedule::default(); + schedule.add_systems(system).run(&mut world); + assert!(world.resource::().0); + + // Test that the handler doesn't change within the schedule + schedule.add_systems( + (|world: &mut World| { + world.insert_resource(DefaultErrorHandler(panic)); + }) + .before(system), + ); + schedule.run(&mut world); + } } diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 896c7ed050..da91f616e9 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -1,4 +1,5 @@ use alloc::boxed::Box; +use bevy_utils::prelude::DebugName; use core::{ any::TypeId, fmt::Debug, @@ -19,7 +20,39 @@ use crate::{ }; define_label!( - /// A strongly-typed class of labels used to identify a [`Schedule`](crate::schedule::Schedule). + /// A strongly-typed class of labels used to identify a [`Schedule`]. + /// + /// Each schedule in a [`World`] has a unique schedule label value, and + /// schedules can be automatically created from labels via [`Schedules::add_systems()`]. + /// + /// # Defining new schedule labels + /// + /// By default, you should use Bevy's premade schedule labels which implement this trait. + /// If you are using [`bevy_ecs`] directly or if you need to run a group of systems outside + /// the existing schedules, you may define your own schedule labels by using + /// `#[derive(ScheduleLabel)]`. + /// + /// ``` + /// use bevy_ecs::prelude::*; + /// use bevy_ecs::schedule::ScheduleLabel; + /// + /// // Declare a new schedule label. + /// #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] + /// struct Update; + /// + /// let mut world = World::new(); + /// + /// // Add a system to the schedule with that label (creating it automatically). + /// fn a_system_function() {} + /// world.get_resource_or_init::().add_systems(Update, a_system_function); + /// + /// // Run the schedule, and therefore run the system. + /// world.run_schedule(Update); + /// ``` + /// + /// [`Schedule`]: crate::schedule::Schedule + /// [`Schedules::add_systems()`]: crate::schedule::Schedules::add_systems + /// [`World`]: crate::world::World #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(ScheduleLabel)]`" )] @@ -28,7 +61,93 @@ define_label!( ); define_label!( - /// Types that identify logical groups of systems. + /// System sets are tag-like labels that can be used to group systems together. + /// + /// This allows you to share configuration (like run conditions) across multiple systems, + /// and order systems or system sets relative to conceptual groups of systems. + /// To control the behavior of a system set as a whole, use [`Schedule::configure_sets`](crate::prelude::Schedule::configure_sets), + /// or the method of the same name on `App`. + /// + /// Systems can belong to any number of system sets, reflecting multiple roles or facets that they might have. + /// For example, you may want to annotate a system as "consumes input" and "applies forces", + /// and ensure that your systems are ordered correctly for both of those sets. + /// + /// System sets can belong to any number of other system sets, + /// allowing you to create nested hierarchies of system sets to group systems together. + /// Configuration applied to system sets will flow down to their members (including other system sets), + /// allowing you to set and modify the configuration in a single place. + /// + /// Systems sets are also useful for exposing a consistent public API for dependencies + /// to hook into across versions of your crate, + /// allowing them to add systems to a specific set, or order relative to that set, + /// without leaking implementation details of the exact systems involved. + /// + /// ## Defining new system sets + /// + /// To create a new system set, use the `#[derive(SystemSet)]` macro. + /// Unit structs are a good choice for one-off sets. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// struct PhysicsSystems; + /// ``` + /// + /// When you want to define several related system sets, + /// consider creating an enum system set. + /// Each variant will be treated as a separate system set. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// ``` + /// + /// By convention, the listed order of the system set in the enum + /// corresponds to the order in which the systems are run. + /// Ordering must be explicitly added to ensure that this is the case, + /// but following this convention will help avoid confusion. + /// + /// ### Adding systems to system sets + /// + /// To add systems to a system set, call [`in_set`](crate::prelude::IntoScheduleConfigs::in_set) on the system function + /// while adding it to your app or schedule. + /// + /// Like usual, these methods can be chained with other configuration methods like [`before`](crate::prelude::IntoScheduleConfigs::before), + /// or repeated to add systems to multiple sets. + /// + /// ```rust + /// use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// + /// fn target_selection() {} + /// + /// fn enemy_damage_calculation() {} + /// + /// fn player_damage_calculation() {} + /// + /// let mut schedule = Schedule::default(); + /// // Configuring the sets to run in order. + /// schedule.configure_sets((CombatSystems::TargetSelection, CombatSystems::DamageCalculation, CombatSystems::Cleanup).chain()); + /// + /// // Adding a single system to a set. + /// schedule.add_systems(target_selection.in_set(CombatSystems::TargetSelection)); + /// + /// // Adding multiple systems to a set. + /// schedule.add_systems((player_damage_calculation, enemy_damage_calculation).in_set(CombatSystems::DamageCalculation)); + /// ``` #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(SystemSet)]`" )] @@ -78,7 +197,7 @@ impl SystemTypeSet { impl Debug for SystemTypeSet { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("SystemTypeSet") - .field(&format_args!("fn {}()", &core::any::type_name::())) + .field(&format_args!("fn {}()", DebugName::type_name::())) .finish() } } @@ -115,15 +234,6 @@ impl SystemSet for SystemTypeSet { fn dyn_clone(&self) -> Box { Box::new(*self) } - - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - - fn dyn_hash(&self, mut state: &mut dyn Hasher) { - TypeId::of::().hash(&mut state); - self.hash(&mut state); - } } /// A [`SystemSet`] implicitly created when using @@ -146,15 +256,6 @@ impl SystemSet for AnonymousSet { fn dyn_clone(&self) -> Box { Box::new(*self) } - - fn as_dyn_eq(&self) -> &dyn DynEq { - self - } - - fn dyn_hash(&self, mut state: &mut dyn Hasher) { - TypeId::of::().hash(&mut state); - self.hash(&mut state); - } } /// Types that can be converted into a [`SystemSet`]. diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index b5df8555e2..b0d8b57fb0 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -125,7 +125,7 @@ impl core::fmt::Debug for Stepping { if self.action != Action::RunAll { let Cursor { schedule, system } = self.cursor; match self.schedule_order.get(schedule) { - Some(label) => write!(f, "cursor: {:?}[{}], ", label, system)?, + Some(label) => write!(f, "cursor: {label:?}[{system}], ")?, None => write!(f, "cursor: None, ")?, }; } @@ -475,9 +475,8 @@ impl Stepping { Some(state) => state.clear_behaviors(), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } }, @@ -486,9 +485,8 @@ impl Stepping { Some(state) => state.set_behavior(system, behavior), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -498,9 +496,8 @@ impl Stepping { Some(state) => state.clear_behavior(system), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -898,9 +895,9 @@ mod tests { ($schedule:expr, $skipped_systems:expr, $($system:expr),*) => { // pull an ordered list of systems in the schedule, and save the // system TypeId, and name. - let systems: Vec<(TypeId, alloc::borrow::Cow<'static, str>)> = $schedule.systems().unwrap() + let systems: Vec<(TypeId, alloc::string::String)> = $schedule.systems().unwrap() .map(|(_, system)| { - (system.type_id(), system.name()) + (system.type_id(), system.name().as_string()) }) .collect(); diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index d5014f2240..0c30c14b9c 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -210,6 +210,7 @@ unsafe impl + Send + Sync + 'static> Bundle ); } } + impl> DynamicBundle for SpawnRelatedBundle { type Effect = Self; diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index caa0785b79..29752ae2e5 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,11 +1,10 @@ use crate::{ - archetype::ArchetypeComponentId, change_detection::{MaybeLocation, MutUntyped, TicksMut}, - component::{ComponentId, ComponentTicks, Components, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentTicks, Components, Tick, TickCells}, storage::{blob_vec::BlobVec, SparseSet}, }; -use alloc::string::String; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, mem::ManuallyDrop, panic::Location}; #[cfg(feature = "std")] @@ -24,8 +23,7 @@ pub struct ResourceData { not(feature = "std"), expect(dead_code, reason = "currently only used with the std feature") )] - type_name: String, - id: ArchetypeComponentId, + type_name: DebugName, #[cfg(feature = "std")] origin_thread_id: Option, changed_by: MaybeLocation>>, @@ -100,12 +98,6 @@ impl ResourceData { !self.data.is_empty() } - /// Gets the [`ArchetypeComponentId`] for the resource. - #[inline] - pub fn id(&self) -> ArchetypeComponentId { - self.id - } - /// Returns a reference to the resource, if it exists. /// /// # Panics @@ -306,9 +298,9 @@ impl ResourceData { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.added_ticks.get_mut().check_tick(change_tick); - self.changed_ticks.get_mut().check_tick(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.added_ticks.get_mut().check_tick(check); + self.changed_ticks.get_mut().check_tick(check); } } @@ -371,7 +363,6 @@ impl Resources { &mut self, component_id: ComponentId, components: &Components, - f: impl FnOnce() -> ArchetypeComponentId, ) -> &mut ResourceData { self.resources.get_or_insert_with(component_id, || { let component_info = components.get_info(component_id).unwrap(); @@ -394,8 +385,7 @@ impl Resources { data: ManuallyDrop::new(data), added_ticks: UnsafeCell::new(Tick::new(0)), changed_ticks: UnsafeCell::new(Tick::new(0)), - type_name: String::from(component_info.name()), - id: f(), + type_name: component_info.name(), #[cfg(feature = "std")] origin_thread_id: None, changed_by: MaybeLocation::caller().map(UnsafeCell::new), @@ -403,9 +393,9 @@ impl Resources { }) } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for info in self.resources.values_mut() { - info.check_change_ticks(change_tick); + info.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 69305e8a14..bb28f967af 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -1,15 +1,13 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, - entity::Entity, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, + entity::{Entity, EntityRow}, storage::{Column, TableRow}, }; use alloc::{boxed::Box, vec::Vec}; use bevy_ptr::{OwningPtr, Ptr}; use core::{cell::UnsafeCell, hash::Hash, marker::PhantomData, panic::Location}; -use nonmax::NonMaxUsize; - -type EntityIndex = u32; +use nonmax::{NonMaxU32, NonMaxUsize}; #[derive(Debug)] pub(crate) struct SparseArray { @@ -121,10 +119,10 @@ pub struct ComponentSparseSet { // stored for entities that are alive. The generation is not required, but is stored // in debug builds to validate that access is correct. #[cfg(not(debug_assertions))] - entities: Vec, + entities: Vec, #[cfg(debug_assertions)] entities: Vec, - sparse: SparseArray, + sparse: SparseArray, } impl ComponentSparseSet { @@ -170,20 +168,25 @@ impl ComponentSparseSet { change_tick: Tick, caller: MaybeLocation, ) { - if let Some(&dense_index) = self.sparse.get(entity.index()) { + if let Some(&dense_index) = self.sparse.get(entity.row()) { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); self.dense.replace(dense_index, value, change_tick, caller); } else { let dense_index = self.dense.len(); self.dense .push(value, ComponentTicks::new(change_tick), caller); - self.sparse - .insert(entity.index(), TableRow::from_usize(dense_index)); + + // SAFETY: This entity row does not exist here yet, so there are no duplicates, + // and the entity index can not be the max, so the length must not be max either. + // To do so would have caused a panic in the entity alloxator. + let table_row = unsafe { TableRow::new(NonMaxU32::new_unchecked(dense_index as u32)) }; + + self.sparse.insert(entity.row(), table_row); #[cfg(debug_assertions)] assert_eq!(self.entities.len(), dense_index); #[cfg(not(debug_assertions))] - self.entities.push(entity.index()); + self.entities.push(entity.row()); #[cfg(debug_assertions)] self.entities.push(entity); } @@ -194,16 +197,16 @@ impl ComponentSparseSet { pub fn contains(&self, entity: Entity) -> bool { #[cfg(debug_assertions)] { - if let Some(&dense_index) = self.sparse.get(entity.index()) { + if let Some(&dense_index) = self.sparse.get(entity.row()) { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); true } else { false } } #[cfg(not(debug_assertions))] - self.sparse.contains(entity.index()) + self.sparse.contains(entity.row()) } /// Returns a reference to the entity's component value. @@ -211,9 +214,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get(&self, entity: Entity) -> Option> { - self.sparse.get(entity.index()).map(|&dense_index| { + self.sparse.get(entity.row()).map(|&dense_index| { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { self.dense.get_data_unchecked(dense_index) } }) @@ -231,9 +234,9 @@ impl ComponentSparseSet { TickCells<'_>, MaybeLocation<&UnsafeCell<&'static Location<'static>>>, )> { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(( @@ -252,9 +255,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_added_tick(&self, entity: Entity) -> Option<&UnsafeCell> { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_added_tick_unchecked(dense_index)) } } @@ -264,9 +267,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_changed_tick(&self, entity: Entity) -> Option<&UnsafeCell> { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_changed_tick_unchecked(dense_index)) } } @@ -276,9 +279,9 @@ impl ComponentSparseSet { /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_ticks(&self, entity: Entity) -> Option { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_ticks_unchecked(dense_index)) } } @@ -292,9 +295,9 @@ impl ComponentSparseSet { entity: Entity, ) -> MaybeLocation>>> { MaybeLocation::new_with_flattened(|| { - let dense_index = *self.sparse.get(entity.index())?; + let dense_index = *self.sparse.get(entity.row())?; #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); + assert_eq!(entity, self.entities[dense_index.index()]); // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { Some(self.dense.get_changed_by_unchecked(dense_index)) } }) @@ -311,19 +314,19 @@ impl ComponentSparseSet { /// it exists). #[must_use = "The returned pointer must be used to drop the removed component."] pub(crate) fn remove_and_forget(&mut self, entity: Entity) -> Option> { - self.sparse.remove(entity.index()).map(|dense_index| { + self.sparse.remove(entity.row()).map(|dense_index| { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); - self.entities.swap_remove(dense_index.as_usize()); - let is_last = dense_index.as_usize() == self.dense.len() - 1; + assert_eq!(entity, self.entities[dense_index.index()]); + self.entities.swap_remove(dense_index.index()); + let is_last = dense_index.index() == self.dense.len() - 1; // SAFETY: dense_index was just removed from `sparse`, which ensures that it is valid let (value, _, _) = unsafe { self.dense.swap_remove_and_forget_unchecked(dense_index) }; if !is_last { - let swapped_entity = self.entities[dense_index.as_usize()]; + let swapped_entity = self.entities[dense_index.index()]; #[cfg(not(debug_assertions))] let index = swapped_entity; #[cfg(debug_assertions)] - let index = swapped_entity.index(); + let index = swapped_entity.row(); *self.sparse.get_mut(index).unwrap() = dense_index; } value @@ -334,21 +337,21 @@ impl ComponentSparseSet { /// /// Returns `true` if `entity` had a component value in the sparse set. pub(crate) fn remove(&mut self, entity: Entity) -> bool { - if let Some(dense_index) = self.sparse.remove(entity.index()) { + if let Some(dense_index) = self.sparse.remove(entity.row()) { #[cfg(debug_assertions)] - assert_eq!(entity, self.entities[dense_index.as_usize()]); - self.entities.swap_remove(dense_index.as_usize()); - let is_last = dense_index.as_usize() == self.dense.len() - 1; + assert_eq!(entity, self.entities[dense_index.index()]); + self.entities.swap_remove(dense_index.index()); + let is_last = dense_index.index() == self.dense.len() - 1; // SAFETY: if the sparse index points to something in the dense vec, it exists unsafe { self.dense.swap_remove_unchecked(dense_index); } if !is_last { - let swapped_entity = self.entities[dense_index.as_usize()]; + let swapped_entity = self.entities[dense_index.index()]; #[cfg(not(debug_assertions))] let index = swapped_entity; #[cfg(debug_assertions)] - let index = swapped_entity.index(); + let index = swapped_entity.row(); *self.sparse.get_mut(index).unwrap() = dense_index; } true @@ -357,8 +360,8 @@ impl ComponentSparseSet { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.dense.check_change_ticks(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.dense.check_change_ticks(check); } } @@ -647,9 +650,9 @@ impl SparseSets { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for set in self.sets.values_mut() { - set.check_change_ticks(change_tick); + set.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 522df222c6..acf531d9b9 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -49,13 +49,13 @@ impl ThinColumn { row: TableRow, ) { self.data - .swap_remove_and_drop_unchecked_nonoverlapping(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked_nonoverlapping(row.index(), last_element_index); self.added_ticks - .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); + .swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); self.changed_ticks - .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); + .swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); self.changed_by.as_mut().map(|changed_by| { - changed_by.swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); + changed_by.swap_remove_unchecked_nonoverlapping(row.index(), last_element_index); }); } @@ -71,13 +71,13 @@ impl ThinColumn { row: TableRow, ) { self.data - .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked(row.index(), last_element_index); self.added_ticks - .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked(row.index(), last_element_index); self.changed_ticks - .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + .swap_remove_and_drop_unchecked(row.index(), last_element_index); self.changed_by.as_mut().map(|changed_by| { - changed_by.swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); + changed_by.swap_remove_and_drop_unchecked(row.index(), last_element_index); }); } @@ -94,14 +94,14 @@ impl ThinColumn { ) { let _ = self .data - .swap_remove_unchecked(row.as_usize(), last_element_index); + .swap_remove_unchecked(row.index(), last_element_index); self.added_ticks - .swap_remove_unchecked(row.as_usize(), last_element_index); + .swap_remove_unchecked(row.index(), last_element_index); self.changed_ticks - .swap_remove_unchecked(row.as_usize(), last_element_index); + .swap_remove_unchecked(row.index(), last_element_index); self.changed_by .as_mut() - .map(|changed_by| changed_by.swap_remove_unchecked(row.as_usize(), last_element_index)); + .map(|changed_by| changed_by.swap_remove_unchecked(row.index(), last_element_index)); } /// Call [`realloc`](std::alloc::realloc) to expand / shrink the memory allocation for this [`ThinColumn`] @@ -148,15 +148,12 @@ impl ThinColumn { tick: Tick, caller: MaybeLocation, ) { - self.data.initialize_unchecked(row.as_usize(), data); - *self.added_ticks.get_unchecked_mut(row.as_usize()).get_mut() = tick; - *self - .changed_ticks - .get_unchecked_mut(row.as_usize()) - .get_mut() = tick; + self.data.initialize_unchecked(row.index(), data); + *self.added_ticks.get_unchecked_mut(row.index()).get_mut() = tick; + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = tick; self.changed_by .as_mut() - .map(|changed_by| changed_by.get_unchecked_mut(row.as_usize()).get_mut()) + .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); } @@ -173,14 +170,11 @@ impl ThinColumn { change_tick: Tick, caller: MaybeLocation, ) { - self.data.replace_unchecked(row.as_usize(), data); - *self - .changed_ticks - .get_unchecked_mut(row.as_usize()) - .get_mut() = change_tick; + self.data.replace_unchecked(row.index(), data); + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = change_tick; self.changed_by .as_mut() - .map(|changed_by| changed_by.get_unchecked_mut(row.as_usize()).get_mut()) + .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); } @@ -206,25 +200,25 @@ impl ThinColumn { // Init the data let src_val = other .data - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); - self.data.initialize_unchecked(dst_row.as_usize(), src_val); + .swap_remove_unchecked(src_row.index(), other_last_element_index); + self.data.initialize_unchecked(dst_row.index(), src_val); // Init added_ticks let added_tick = other .added_ticks - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); + .swap_remove_unchecked(src_row.index(), other_last_element_index); self.added_ticks - .initialize_unchecked(dst_row.as_usize(), added_tick); + .initialize_unchecked(dst_row.index(), added_tick); // Init changed_ticks let changed_tick = other .changed_ticks - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); + .swap_remove_unchecked(src_row.index(), other_last_element_index); self.changed_ticks - .initialize_unchecked(dst_row.as_usize(), changed_tick); + .initialize_unchecked(dst_row.index(), changed_tick); self.changed_by.as_mut().zip(other.changed_by.as_mut()).map( |(self_changed_by, other_changed_by)| { let changed_by = other_changed_by - .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); - self_changed_by.initialize_unchecked(dst_row.as_usize(), changed_by); + .swap_remove_unchecked(src_row.index(), other_last_element_index); + self_changed_by.initialize_unchecked(dst_row.index(), changed_by); }, ); } @@ -234,20 +228,20 @@ impl ThinColumn { /// # Safety /// `len` is the actual length of this column #[inline] - pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, change_tick: Tick) { + pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, check: CheckChangeTicks) { for i in 0..len { // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.added_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.changed_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); } } @@ -384,15 +378,12 @@ impl Column { change_tick: Tick, caller: MaybeLocation, ) { - debug_assert!(row.as_usize() < self.len()); - self.data.replace_unchecked(row.as_usize(), data); - *self - .changed_ticks - .get_unchecked_mut(row.as_usize()) - .get_mut() = change_tick; + debug_assert!(row.index() < self.len()); + self.data.replace_unchecked(row.index(), data); + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = change_tick; self.changed_by .as_mut() - .map(|changed_by| changed_by.get_unchecked_mut(row.as_usize()).get_mut()) + .map(|changed_by| changed_by.get_unchecked_mut(row.index()).get_mut()) .assign(caller); } @@ -419,12 +410,12 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) { - self.data.swap_remove_and_drop_unchecked(row.as_usize()); - self.added_ticks.swap_remove(row.as_usize()); - self.changed_ticks.swap_remove(row.as_usize()); + self.data.swap_remove_and_drop_unchecked(row.index()); + self.added_ticks.swap_remove(row.index()); + self.changed_ticks.swap_remove(row.index()); self.changed_by .as_mut() - .map(|changed_by| changed_by.swap_remove(row.as_usize())); + .map(|changed_by| changed_by.swap_remove(row.index())); } /// Removes an element from the [`Column`] and returns it and its change detection ticks. @@ -444,13 +435,13 @@ impl Column { &mut self, row: TableRow, ) -> (OwningPtr<'_>, ComponentTicks, MaybeLocation) { - let data = self.data.swap_remove_and_forget_unchecked(row.as_usize()); - let added = self.added_ticks.swap_remove(row.as_usize()).into_inner(); - let changed = self.changed_ticks.swap_remove(row.as_usize()).into_inner(); + let data = self.data.swap_remove_and_forget_unchecked(row.index()); + let added = self.added_ticks.swap_remove(row.index()).into_inner(); + let changed = self.changed_ticks.swap_remove(row.index()).into_inner(); let caller = self .changed_by .as_mut() - .map(|changed_by| changed_by.swap_remove(row.as_usize()).into_inner()); + .map(|changed_by| changed_by.swap_remove(row.index()).into_inner()); (data, ComponentTicks { added, changed }, caller) } @@ -520,15 +511,15 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get(&self, row: TableRow) -> Option<(Ptr<'_>, TickCells<'_>)> { - (row.as_usize() < self.data.len()) + (row.index() < self.data.len()) // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through a read-only reference to the column. .then(|| unsafe { ( - self.data.get_unchecked(row.as_usize()), + self.data.get_unchecked(row.index()), TickCells { - added: self.added_ticks.get_unchecked(row.as_usize()), - changed: self.changed_ticks.get_unchecked(row.as_usize()), + added: self.added_ticks.get_unchecked(row.index()), + changed: self.changed_ticks.get_unchecked(row.index()), }, ) }) @@ -539,10 +530,10 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_data(&self, row: TableRow) -> Option> { - (row.as_usize() < self.data.len()).then(|| { + (row.index() < self.data.len()).then(|| { // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through a read-only reference to the column. - unsafe { self.data.get_unchecked(row.as_usize()) } + unsafe { self.data.get_unchecked(row.index()) } }) } @@ -554,8 +545,8 @@ impl Column { /// - no other mutable reference to the data of the same row can exist at the same time #[inline] pub unsafe fn get_data_unchecked(&self, row: TableRow) -> Ptr<'_> { - debug_assert!(row.as_usize() < self.data.len()); - self.data.get_unchecked(row.as_usize()) + debug_assert!(row.index() < self.data.len()); + self.data.get_unchecked(row.index()) } /// Fetches a mutable reference to the data at `row`. @@ -563,10 +554,10 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_data_mut(&mut self, row: TableRow) -> Option> { - (row.as_usize() < self.data.len()).then(|| { + (row.index() < self.data.len()).then(|| { // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through an exclusive reference to the column. - unsafe { self.data.get_unchecked_mut(row.as_usize()) } + unsafe { self.data.get_unchecked_mut(row.index()) } }) } @@ -579,7 +570,7 @@ impl Column { /// adhere to the safety invariants of [`UnsafeCell`]. #[inline] pub fn get_added_tick(&self, row: TableRow) -> Option<&UnsafeCell> { - self.added_ticks.get(row.as_usize()) + self.added_ticks.get(row.index()) } /// Fetches the "changed" change detection tick for the value at `row`. @@ -591,7 +582,7 @@ impl Column { /// adhere to the safety invariants of [`UnsafeCell`]. #[inline] pub fn get_changed_tick(&self, row: TableRow) -> Option<&UnsafeCell> { - self.changed_ticks.get(row.as_usize()) + self.changed_ticks.get(row.index()) } /// Fetches the change detection ticks for the value at `row`. @@ -599,7 +590,7 @@ impl Column { /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_ticks(&self, row: TableRow) -> Option { - if row.as_usize() < self.data.len() { + if row.index() < self.data.len() { // SAFETY: The size of the column has already been checked. Some(unsafe { self.get_ticks_unchecked(row) }) } else { @@ -614,8 +605,8 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_added_tick_unchecked(&self, row: TableRow) -> &UnsafeCell { - debug_assert!(row.as_usize() < self.added_ticks.len()); - self.added_ticks.get_unchecked(row.as_usize()) + debug_assert!(row.index() < self.added_ticks.len()); + self.added_ticks.get_unchecked(row.index()) } /// Fetches the "changed" change detection tick for the value at `row`. Unlike [`Column::get_changed_tick`] @@ -625,8 +616,8 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_changed_tick_unchecked(&self, row: TableRow) -> &UnsafeCell { - debug_assert!(row.as_usize() < self.changed_ticks.len()); - self.changed_ticks.get_unchecked(row.as_usize()) + debug_assert!(row.index() < self.changed_ticks.len()); + self.changed_ticks.get_unchecked(row.index()) } /// Fetches the change detection ticks for the value at `row`. Unlike [`Column::get_ticks`] @@ -636,11 +627,11 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_ticks_unchecked(&self, row: TableRow) -> ComponentTicks { - debug_assert!(row.as_usize() < self.added_ticks.len()); - debug_assert!(row.as_usize() < self.changed_ticks.len()); + debug_assert!(row.index() < self.added_ticks.len()); + debug_assert!(row.index() < self.changed_ticks.len()); ComponentTicks { - added: self.added_ticks.get_unchecked(row.as_usize()).read(), - changed: self.changed_ticks.get_unchecked(row.as_usize()).read(), + added: self.added_ticks.get_unchecked(row.index()).read(), + changed: self.changed_ticks.get_unchecked(row.index()).read(), } } @@ -655,12 +646,12 @@ impl Column { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for component_ticks in &mut self.added_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } for component_ticks in &mut self.changed_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } } @@ -678,7 +669,7 @@ impl Column { ) -> MaybeLocation>>> { self.changed_by .as_ref() - .map(|changed_by| changed_by.get(row.as_usize())) + .map(|changed_by| changed_by.get(row.index())) } /// Fetches the calling location that last changed the value at `row`. @@ -693,8 +684,8 @@ impl Column { row: TableRow, ) -> MaybeLocation<&UnsafeCell<&'static Location<'static>>> { self.changed_by.as_ref().map(|changed_by| { - debug_assert!(row.as_usize() < changed_by.len()); - changed_by.get_unchecked(row.as_usize()) + debug_assert!(row.index() < changed_by.len()); + changed_by.get_unchecked(row.index()) }) } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index fd8eb410e4..be75c58f03 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, entity::Entity, query::DebugCheckedUnwrap, storage::{blob_vec::BlobVec, ImmutableSparseSet, SparseSet}, @@ -16,6 +16,7 @@ use core::{ ops::{Index, IndexMut}, panic::Location, }; +use nonmax::NonMaxU32; mod column; /// An opaque unique ID for a [`Table`] within a [`World`]. @@ -35,8 +36,6 @@ mod column; pub struct TableId(u32); impl TableId { - pub(crate) const INVALID: TableId = TableId(u32::MAX); - /// Creates a new [`TableId`]. /// /// `index` *must* be retrieved from calling [`TableId::as_u32`] on a `TableId` you got @@ -100,39 +99,27 @@ impl TableId { /// [`Archetype::entity_table_row`]: crate::archetype::Archetype::entity_table_row /// [`Archetype::table_id`]: crate::archetype::Archetype::table_id #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TableRow(u32); +#[repr(transparent)] +pub struct TableRow(NonMaxU32); impl TableRow { - pub(crate) const INVALID: TableRow = TableRow(u32::MAX); - - /// Creates a `TableRow`. + /// Creates a [`TableRow`]. #[inline] - pub const fn from_u32(index: u32) -> Self { + pub const fn new(index: NonMaxU32) -> Self { Self(index) } - /// Creates a `TableRow` from a [`usize`] index. - /// - /// # Panics - /// - /// Will panic in debug mode if the provided value does not fit within a [`u32`]. - #[inline] - pub const fn from_usize(index: usize) -> Self { - debug_assert!(index as u32 as usize == index); - Self(index as u32) - } - /// Gets the index of the row as a [`usize`]. #[inline] - pub const fn as_usize(self) -> usize { + pub const fn index(self) -> usize { // usize is at least u32 in Bevy - self.0 as usize + self.0.get() as usize } /// Gets the index of the row as a [`usize`]. #[inline] - pub const fn as_u32(self) -> u32 { - self.0 + pub const fn index_u32(self) -> u32 { + self.0.get() } } @@ -225,9 +212,9 @@ impl Table { /// # Safety /// `row` must be in-bounds (`row.as_usize()` < `self.len()`) pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) -> Option { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - if row.as_usize() != last_element_index { + if row.index_u32() != last_element_index { // Instead of checking this condition on every `swap_remove` call, we // check it here and use `swap_remove_nonoverlapping`. for col in self.columns.values_mut() { @@ -237,22 +224,26 @@ impl Table { // - `row` != `last_element_index` // - the `len` is kept within `self.entities`, it will update accordingly. unsafe { - col.swap_remove_and_drop_unchecked_nonoverlapping(last_element_index, row); + col.swap_remove_and_drop_unchecked_nonoverlapping( + last_element_index as usize, + row, + ); }; } } else { // If `row.as_usize()` == `last_element_index` than there's no point in removing the component // at `row`, but we still need to drop it. for col in self.columns.values_mut() { - col.drop_last_component(last_element_index); + col.drop_last_component(last_element_index as usize); } } - let is_last = row.as_usize() == last_element_index; - self.entities.swap_remove(row.as_usize()); + let is_last = row.index_u32() == last_element_index; + self.entities.swap_remove(row.index()); if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } } } @@ -269,16 +260,21 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - let is_last = row.as_usize() == last_element_index; - let new_row = new_table.allocate(self.entities.swap_remove(row.as_usize())); + let is_last = row.index_u32() == last_element_index; + let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { if let Some(new_column) = new_table.get_column_mut(*component_id) { - new_column.initialize_from_unchecked(column, last_element_index, row, new_row); + new_column.initialize_from_unchecked( + column, + last_element_index as usize, + row, + new_row, + ); } else { // It's the caller's responsibility to drop these cases. - column.swap_remove_and_forget_unchecked(last_element_index, row); + column.swap_remove_and_forget_unchecked(last_element_index as usize, row); } } TableMoveResult { @@ -286,7 +282,8 @@ impl Table { swapped_entity: if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } }, } } @@ -302,15 +299,20 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - let is_last = row.as_usize() == last_element_index; - let new_row = new_table.allocate(self.entities.swap_remove(row.as_usize())); + let is_last = row.index_u32() == last_element_index; + let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { if let Some(new_column) = new_table.get_column_mut(*component_id) { - new_column.initialize_from_unchecked(column, last_element_index, row, new_row); + new_column.initialize_from_unchecked( + column, + last_element_index as usize, + row, + new_row, + ); } else { - column.swap_remove_and_drop_unchecked(last_element_index, row); + column.swap_remove_and_drop_unchecked(last_element_index as usize, row); } } TableMoveResult { @@ -318,7 +320,8 @@ impl Table { swapped_entity: if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } }, } } @@ -335,22 +338,23 @@ impl Table { row: TableRow, new_table: &mut Table, ) -> TableMoveResult { - debug_assert!(row.as_usize() < self.entity_count()); + debug_assert!(row.index_u32() < self.entity_count()); let last_element_index = self.entity_count() - 1; - let is_last = row.as_usize() == last_element_index; - let new_row = new_table.allocate(self.entities.swap_remove(row.as_usize())); + let is_last = row.index_u32() == last_element_index; + let new_row = new_table.allocate(self.entities.swap_remove(row.index())); for (component_id, column) in self.columns.iter_mut() { new_table .get_column_mut(*component_id) .debug_checked_unwrap() - .initialize_from_unchecked(column, last_element_index, row, new_row); + .initialize_from_unchecked(column, last_element_index as usize, row, new_row); } TableMoveResult { new_row, swapped_entity: if is_last { None } else { - Some(self.entities[row.as_usize()]) + // SAFETY: This was sawp removed and was not last, so it must be in bounds. + unsafe { Some(*self.entities.get_unchecked(row.index())) } }, } } @@ -365,7 +369,7 @@ impl Table { component_id: ComponentId, ) -> Option<&[UnsafeCell]> { self.get_column(component_id) - .map(|col| col.get_data_slice(self.entity_count())) + .map(|col| col.get_data_slice(self.entity_count() as usize)) } /// Get the added ticks of the column matching `component_id` as a slice. @@ -375,7 +379,7 @@ impl Table { ) -> Option<&[UnsafeCell]> { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the ticks array - .map(|col| unsafe { col.get_added_ticks_slice(self.entity_count()) }) + .map(|col| unsafe { col.get_added_ticks_slice(self.entity_count() as usize) }) } /// Get the changed ticks of the column matching `component_id` as a slice. @@ -385,7 +389,7 @@ impl Table { ) -> Option<&[UnsafeCell]> { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the ticks array - .map(|col| unsafe { col.get_changed_ticks_slice(self.entity_count()) }) + .map(|col| unsafe { col.get_changed_ticks_slice(self.entity_count() as usize) }) } /// Fetches the calling locations that last changed the each component @@ -396,7 +400,7 @@ impl Table { MaybeLocation::new_with_flattened(|| { self.get_column(component_id) // SAFETY: `self.len()` is guaranteed to be the len of the locations array - .map(|col| unsafe { col.get_changed_by_slice(self.entity_count()) }) + .map(|col| unsafe { col.get_changed_by_slice(self.entity_count() as usize) }) }) } @@ -406,12 +410,12 @@ impl Table { component_id: ComponentId, row: TableRow, ) -> Option<&UnsafeCell> { - (row.as_usize() < self.entity_count()).then_some( + (row.index_u32() < self.entity_count()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? .changed_ticks - .get_unchecked(row.as_usize()) + .get_unchecked(row.index()) }, ) } @@ -422,12 +426,12 @@ impl Table { component_id: ComponentId, row: TableRow, ) -> Option<&UnsafeCell> { - (row.as_usize() < self.entity_count()).then_some( + (row.index_u32() < self.entity_count()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? .added_ticks - .get_unchecked(row.as_usize()) + .get_unchecked(row.index()) }, ) } @@ -439,13 +443,13 @@ impl Table { row: TableRow, ) -> MaybeLocation>>> { MaybeLocation::new_with_flattened(|| { - (row.as_usize() < self.entity_count()).then_some( + (row.index_u32() < self.entity_count()).then_some( // SAFETY: `row.as_usize()` < `len` unsafe { self.get_column(component_id)? .changed_by .as_ref() - .map(|changed_by| changed_by.get_unchecked(row.as_usize())) + .map(|changed_by| changed_by.get_unchecked(row.index())) }, ) }) @@ -461,8 +465,8 @@ impl Table { row: TableRow, ) -> Option { self.get_column(component_id).map(|col| ComponentTicks { - added: col.added_ticks.get_unchecked(row.as_usize()).read(), - changed: col.changed_ticks.get_unchecked(row.as_usize()).read(), + added: col.added_ticks.get_unchecked(row.index()).read(), + changed: col.changed_ticks.get_unchecked(row.index()).read(), }) } @@ -499,7 +503,7 @@ impl Table { /// Reserves `additional` elements worth of capacity within the table. pub(crate) fn reserve(&mut self, additional: usize) { - if self.capacity() - self.entity_count() < additional { + if (self.capacity() - self.entity_count() as usize) < additional { let column_cap = self.capacity(); self.entities.reserve(additional); @@ -563,10 +567,15 @@ impl Table { /// Allocates space for a new entity /// /// # Safety - /// the allocated row must be written to immediately with valid values in each column + /// + /// The allocated row must be written to immediately with valid values in each column pub(crate) unsafe fn allocate(&mut self, entity: Entity) -> TableRow { self.reserve(1); let len = self.entity_count(); + // SAFETY: No entity row may be in more than one table row at once, so there are no duplicates, + // and there can not be an entity row of u32::MAX. Therefore, this can not be max either. + let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(len)) }; + let len = len as usize; self.entities.push(entity); for col in self.columns.values_mut() { col.added_ticks @@ -580,13 +589,16 @@ impl Table { changed_by.initialize_unchecked(len, UnsafeCell::new(caller)); }); } - TableRow::from_usize(len) + + row } /// Gets the number of entities currently being stored in the table. #[inline] - pub fn entity_count(&self) -> usize { - self.entities.len() + pub fn entity_count(&self) -> u32 { + // No entity may have more than one table row, so there are no duplicates, + // and there may only ever be u32::MAX entities, so the length never exceeds u32's cappacity. + self.entities.len() as u32 } /// Get the drop function for some component that is stored in this table. @@ -617,11 +629,11 @@ impl Table { } /// Call [`Tick::check_tick`] on all of the ticks in the [`Table`] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - let len = self.entity_count(); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + let len = self.entity_count() as usize; for col in self.columns.values_mut() { // SAFETY: `len` is the actual length of the column - unsafe { col.check_change_ticks(len, change_tick) }; + unsafe { col.check_change_ticks(len, check) }; } } @@ -632,7 +644,7 @@ impl Table { /// Clears all of the stored components in the [`Table`]. pub(crate) fn clear(&mut self) { - let len = self.entity_count(); + let len = self.entity_count() as usize; // We must clear the entities first, because in the drop function causes a panic, it will result in a double free of the columns. self.entities.clear(); for column in self.columns.values_mut() { @@ -660,7 +672,7 @@ impl Table { self.get_column_mut(component_id) .debug_checked_unwrap() .data - .get_unchecked_mut(row.as_usize()) + .get_unchecked_mut(row.index()) .promote() } @@ -674,7 +686,7 @@ impl Table { row: TableRow, ) -> Option> { self.get_column(component_id) - .map(|col| col.data.get_unchecked(row.as_usize())) + .map(|col| col.data.get_unchecked(row.index())) } } @@ -781,9 +793,9 @@ impl Tables { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for table in &mut self.tables { - table.check_change_ticks(change_tick); + table.check_change_ticks(check); } } } @@ -806,7 +818,7 @@ impl IndexMut for Tables { impl Drop for Table { fn drop(&mut self) { - let len = self.entity_count(); + let len = self.entity_count() as usize; let cap = self.capacity(); self.entities.clear(); for col in self.columns.values_mut() { diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 5953a43d70..c655ed9407 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -1,4 +1,5 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use super::{IntoSystem, ReadOnlySystem, System, SystemParamValidationError}; use crate::{ @@ -101,7 +102,7 @@ where pub struct AdapterSystem { func: Func, system: S, - name: Cow<'static, str>, + name: DebugName, } impl AdapterSystem @@ -110,7 +111,7 @@ where S: System, { /// Creates a new [`System`] that uses `func` to adapt `system`, via the [`Adapt`] trait. - pub const fn new(func: Func, system: S, name: Cow<'static, str>) -> Self { + pub const fn new(func: Func, system: S, name: DebugName) -> Self { Self { func, system, name } } } @@ -123,37 +124,13 @@ where type In = Func::In; type Out = Func::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access(&self) -> &crate::query::Access { - self.system.component_access() - } - - fn component_access_set( - &self, - ) -> &crate::query::FilteredAccessSet { - self.system.component_access_set() - } - #[inline] - fn archetype_component_access( - &self, - ) -> &crate::query::Access { - self.system.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + fn flags(&self) -> super::SystemStateFlags { + self.system.flags() } #[inline] @@ -168,6 +145,12 @@ where }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut crate::prelude::World) { self.system.apply_deferred(world); @@ -187,17 +170,15 @@ where unsafe { self.system.validate_param_unsafe(world) } } - fn initialize(&mut self, world: &mut crate::prelude::World) { - self.system.initialize(world); + fn initialize( + &mut self, + world: &mut crate::prelude::World, + ) -> crate::query::FilteredAccessSet { + self.system.initialize(world) } - #[inline] - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.system.update_archetype_component_access(world); - } - - fn check_change_tick(&mut self, change_tick: crate::component::Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: crate::component::CheckChangeTicks) { + self.system.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 441d4d42fc..7aea731314 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -1,5 +1,5 @@ use alloc::{boxed::Box, vec::Vec}; -use bevy_utils::synccell::SyncCell; +use bevy_platform::cell::SyncCell; use variadics_please::all_tuples; use crate::{ @@ -7,7 +7,7 @@ use crate::{ query::{QueryData, QueryFilter, QueryState}, resource::Resource, system::{ - DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, + DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemParam, SystemParamValidationError, When, }, world::{ @@ -17,7 +17,7 @@ use crate::{ }; use core::fmt::Debug; -use super::{init_query_param, Res, ResMut, SystemState}; +use super::{Res, ResMut, SystemState}; /// A builder that can create a [`SystemParam`]. /// @@ -104,19 +104,15 @@ use super::{init_query_param, Res, ResMut, SystemState}; /// /// # Safety /// -/// The implementor must ensure the following is true. -/// - [`SystemParamBuilder::build`] correctly registers all [`World`] accesses used -/// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). -/// - None of the world accesses may conflict with any prior accesses registered -/// on `system_meta`. -/// -/// Note that this depends on the implementation of [`SystemParam::get_param`], +/// The implementor must ensure that the state returned +/// from [`SystemParamBuilder::build`] is valid for `P`. +/// Note that the exact safety requiremensts depend on the implementation of [`SystemParam`], /// so if `Self` is not a local type then you must call [`SystemParam::init_state`] -/// or another [`SystemParamBuilder::build`] +/// or another [`SystemParamBuilder::build`]. pub unsafe trait SystemParamBuilder: Sized { /// Registers any [`World`] access used by this [`SystemParam`] /// and creates a new instance of this param's [`State`](SystemParam::State). - fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State; + fn build(self, world: &mut World) -> P::State; /// Create a [`SystemState`] from a [`SystemParamBuilder`]. /// To create a system, call [`SystemState::build_system`] on the result. @@ -169,8 +165,8 @@ pub struct ParamBuilder; // SAFETY: Calls `SystemParam::init_state` unsafe impl SystemParamBuilder

for ParamBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State { - P::init_state(world, meta) + fn build(self, world: &mut World) -> P::State { + P::init_state(world) } } @@ -208,13 +204,13 @@ impl ParamBuilder { } } -// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +// SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, +// and we check the world during `build`. unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> SystemParamBuilder> for QueryState { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + fn build(self, world: &mut World) -> QueryState { self.validate_world(world.id()); - init_query_param(world, system_meta, &self); self } } @@ -282,7 +278,8 @@ impl<'a, D: QueryData, F: QueryFilter> } } -// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +// SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, +// and `QueryBuilder` produces one with the given `world`. unsafe impl< 'w, 's, @@ -291,12 +288,10 @@ unsafe impl< T: FnOnce(&mut QueryBuilder), > SystemParamBuilder> for QueryParamBuilder { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + fn build(self, world: &mut World) -> QueryState { let mut builder = QueryBuilder::new(world); (self.0)(&mut builder); - let state = builder.build(); - init_query_param(world, system_meta, &state); - state + builder.build() } } @@ -317,13 +312,13 @@ macro_rules! impl_system_param_builder_tuple { $(#[$meta])* // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<$($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder<($($param,)*)> for ($($builder,)*) { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World) -> <($($param,)*) as SystemParam>::State { let ($($builder,)*) = self; #[allow( clippy::unused_unit, reason = "Zero-length tuples won't generate any calls to the system parameter builders." )] - ($($builder.build(world, meta),)*) + ($($builder.build(world),)*) } } }; @@ -340,9 +335,9 @@ all_tuples!( // SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl> SystemParamBuilder> for Vec { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { self.into_iter() - .map(|builder| builder.build(world, meta)) + .map(|builder| builder.build(world)) .collect() } } @@ -422,7 +417,7 @@ unsafe impl> SystemParamBuilder> pub struct ParamSetBuilder(pub T); macro_rules! impl_param_set_builder_tuple { - ($(($param: ident, $builder: ident, $meta: ident)),*) => { + ($(($param: ident, $builder: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is in a macro; as such, the below lints may not always apply." @@ -437,85 +432,38 @@ macro_rules! impl_param_set_builder_tuple { )] // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<'w, 's, $($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder> for ParamSetBuilder<($($builder,)*)> { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World) -> <($($param,)*) as SystemParam>::State { let ParamSetBuilder(($($builder,)*)) = self; - // Note that this is slightly different from `init_state`, which calls `init_state` on each param twice. - // One call populates an empty `SystemMeta` with the new access, while the other runs against a cloned `SystemMeta` to check for conflicts. - // Builders can only be invoked once, so we do both in a single call here. - // That means that any `filtered_accesses` in the `component_access_set` will get copied to every `$meta` - // and will appear multiple times in the final `SystemMeta`. - $( - let mut $meta = system_meta.clone(); - let $param = $builder.build(world, &mut $meta); - )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false $(|| !$meta.is_send())* { - system_meta.set_non_send(); - } - $( - system_meta - .component_access_set - .extend($meta.component_access_set); - system_meta - .archetype_component_access - .extend(&$meta.archetype_component_access); - )* - #[allow( - clippy::unused_unit, - reason = "Zero-length tuples won't generate any calls to the system parameter builders." - )] - ($($param,)*) + ($($builder.build(world),)*) } } }; } -all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta); +all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B); -// SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts -// with any prior access, a panic will occur. +// SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl<'w, 's, P: SystemParam, B: SystemParamBuilder

> SystemParamBuilder>> for ParamSetBuilder> { - fn build( - self, - world: &mut World, - system_meta: &mut SystemMeta, - ) -> as SystemParam>::State { - let mut states = Vec::with_capacity(self.0.len()); - let mut metas = Vec::with_capacity(self.0.len()); - for builder in self.0 { - let mut meta = system_meta.clone(); - states.push(builder.build(world, &mut meta)); - metas.push(meta); - } - if metas.iter().any(|m| !m.is_send()) { - system_meta.set_non_send(); - } - for meta in metas { - system_meta - .component_access_set - .extend(meta.component_access_set); - system_meta - .archetype_component_access - .extend(&meta.archetype_component_access); - } - states + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0 + .into_iter() + .map(|builder| builder.build(world)) + .collect() } } /// A [`SystemParamBuilder`] for a [`DynSystemParam`]. /// See the [`DynSystemParam`] docs for examples. -pub struct DynParamBuilder<'a>( - Box DynSystemParamState + 'a>, -); +pub struct DynParamBuilder<'a>(Box DynSystemParamState + 'a>); impl<'a> DynParamBuilder<'a> { /// Creates a new [`DynParamBuilder`] by wrapping a [`SystemParamBuilder`] of any type. /// The built [`DynSystemParam`] can be downcast to `T`. pub fn new(builder: impl SystemParamBuilder + 'a) -> Self { - Self(Box::new(|world, meta| { - DynSystemParamState::new::(builder.build(world, meta)) + Self(Box::new(|world| { + DynSystemParamState::new::(builder.build(world)) })) } } @@ -524,12 +472,8 @@ impl<'a> DynParamBuilder<'a> { // and the boxed builder was a valid implementation of `SystemParamBuilder` for that type. // The resulting `DynSystemParam` can only perform access by downcasting to that param type. unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamBuilder<'a> { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { - (self.0)(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + (self.0)(world) } } @@ -555,15 +499,11 @@ unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamB #[derive(Default, Debug, Clone)] pub struct LocalBuilder(pub T); -// SAFETY: `Local` performs no world access. +// SAFETY: Any value of `T` is a valid state for `Local`. unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> for LocalBuilder { - fn build( - self, - _world: &mut World, - _meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, _world: &mut World) -> as SystemParam>::State { SyncCell::new(self.0) } } @@ -591,44 +531,14 @@ impl<'a> FilteredResourcesParamBuilder SystemParamBuilder> for FilteredResourcesParamBuilder { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { let mut builder = FilteredResourcesBuilder::new(world); (self.0)(&mut builder); - let access = builder.build(); - - let combined_access = meta.component_access_set.combined_access(); - let conflicts = combined_access.get_conflicts(&access); - if !conflicts.is_empty() { - let accesses = conflicts.format_conflict_list(world); - let system_name = &meta.name; - panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - meta.archetype_component_access.read_all_resources(); - } else { - for component_id in access.resource_reads_and_writes() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - - let archetype_component_id = world.initialize_resource_internal(component_id).id(); - meta.archetype_component_access - .add_resource_read(archetype_component_id); - } - } - - access + builder.build() } } @@ -655,59 +565,14 @@ impl<'a> FilteredResourcesMutParamBuilder SystemParamBuilder> for FilteredResourcesMutParamBuilder { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { let mut builder = FilteredResourcesMutBuilder::new(world); (self.0)(&mut builder); - let access = builder.build(); - - let combined_access = meta.component_access_set.combined_access(); - let conflicts = combined_access.get_conflicts(&access); - if !conflicts.is_empty() { - let accesses = conflicts.format_conflict_list(world); - let system_name = &meta.name; - panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - meta.archetype_component_access.read_all_resources(); - } else { - for component_id in access.resource_reads() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - - let archetype_component_id = world.initialize_resource_internal(component_id).id(); - meta.archetype_component_access - .add_resource_read(archetype_component_id); - } - } - - if access.has_write_all_resources() { - meta.component_access_set - .add_unfiltered_write_all_resources(); - meta.archetype_component_access.write_all_resources(); - } else { - for component_id in access.resource_writes() { - meta.component_access_set - .add_unfiltered_resource_write(component_id); - - let archetype_component_id = world.initialize_resource_internal(component_id).id(); - meta.archetype_component_access - .add_resource_write(archetype_component_id); - } - } - - access + builder.build() } } @@ -719,8 +584,8 @@ pub struct OptionBuilder(T); unsafe impl> SystemParamBuilder> for OptionBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { - self.0.build(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0.build(world) } } @@ -735,9 +600,8 @@ unsafe impl> fn build( self, world: &mut World, - meta: &mut SystemMeta, ) -> as SystemParam>::State { - self.0.build(world, meta) + self.0.build(world) } } @@ -749,8 +613,8 @@ pub struct WhenBuilder(T); unsafe impl> SystemParamBuilder> for WhenBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { - self.0.build(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0.build(world) } } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 9d11de9525..d48c599f2d 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -1,11 +1,11 @@ -use alloc::{borrow::Cow, format, vec::Vec}; +use alloc::{format, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::World, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn, SystemParamValidationError}, world::unsafe_world_cell::UnsafeWorldCell, @@ -56,7 +56,7 @@ use super::{IntoSystem, ReadOnlySystem, System}; /// IntoSystem::into_system(resource_equals(A(1))), /// IntoSystem::into_system(resource_equals(B(1))), /// // The name of the combined system. -/// std::borrow::Cow::Borrowed("a ^ b"), +/// "a ^ b".into(), /// ))); /// # fn my_system(mut flag: ResMut) { flag.0 = true; } /// # @@ -113,23 +113,19 @@ pub struct CombinatorSystem { _marker: PhantomData Func>, a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, - archetype_component_access: Access, + name: DebugName, } impl CombinatorSystem { /// Creates a new system that combines two inner systems. /// /// The returned system will only be usable if `Func` implements [`Combine`]. - pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { + pub fn new(a: A, b: B, name: DebugName) -> Self { Self { _marker: PhantomData, a, b, name, - component_access_set: FilteredAccessSet::default(), - archetype_component_access: Access::new(), } } } @@ -143,32 +139,13 @@ where type In = Func::In; type Out = Func::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -183,14 +160,19 @@ where // If either system has `is_exclusive()`, then the combined system also has `is_exclusive`. // Since these closures are `!Send + !Sync + !'static`, they can never be called // in parallel, so their world accesses will not conflict with each other. - // Additionally, `update_archetype_component_access` has been called, - // which forwards to the implementations for `self.a` and `self.b`. |input| unsafe { self.a.run_unsafe(input, world) }, // SAFETY: See the comment above. |input| unsafe { self.b.run_unsafe(input, world) }, ) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.a.refresh_hotpatch(); + self.b.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.a.apply_deferred(world); @@ -212,28 +194,16 @@ where unsafe { self.a.validate_param_unsafe(world) } } - fn initialize(&mut self, world: &mut World) { - self.a.initialize(world); - self.b.initialize(world); - self.component_access_set - .extend(self.a.component_access_set().clone()); - self.component_access_set - .extend(self.b.component_access_set().clone()); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let mut a_access = self.a.initialize(world); + let b_access = self.b.initialize(world); + a_access.extend(b_access); + a_access } - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.a.update_archetype_component_access(world); - self.b.update_archetype_component_access(world); - - self.archetype_component_access - .extend(self.a.archetype_component_access()); - self.archetype_component_access - .extend(self.b.archetype_component_access()); - } - - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { @@ -302,7 +272,7 @@ where let system_a = IntoSystem::into_system(this.a); let system_b = IntoSystem::into_system(this.b); let name = format!("Pipe({}, {})", system_a.name(), system_b.name()); - PipeSystem::new(system_a, system_b, Cow::Owned(name)) + PipeSystem::new(system_a, system_b, DebugName::owned(name)) } } @@ -348,9 +318,7 @@ where pub struct PipeSystem { a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, - archetype_component_access: Access, + name: DebugName, } impl PipeSystem @@ -360,14 +328,8 @@ where for<'a> B::In: SystemInput = A::Out>, { /// Creates a new system that pipes two inner systems. - pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { - Self { - a, - b, - name, - component_access_set: FilteredAccessSet::default(), - archetype_component_access: Access::new(), - } + pub fn new(a: A, b: B, name: DebugName) -> Self { + Self { a, b, name } } } @@ -380,32 +342,13 @@ where type In = A::In; type Out = B::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -417,6 +360,13 @@ where self.b.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.a.refresh_hotpatch(); + self.b.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.a.apply_deferred(world); self.b.apply_deferred(world); @@ -450,28 +400,16 @@ where Ok(()) } - fn initialize(&mut self, world: &mut World) { - self.a.initialize(world); - self.b.initialize(world); - self.component_access_set - .extend(self.a.component_access_set().clone()); - self.component_access_set - .extend(self.b.component_access_set().clone()); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let mut a_access = self.a.initialize(world); + let b_access = self.b.initialize(world); + a_access.extend(b_access); + a_access } - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.a.update_archetype_component_access(world); - self.b.update_archetype_component_access(world); - - self.archetype_component_access - .extend(self.a.archetype_component_access()); - self.archetype_component_access - .extend(self.b.archetype_component_access()); - } - - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index af7b88edfc..5f1f611856 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -9,7 +9,7 @@ use crate::{ change_detection::MaybeLocation, entity::Entity, error::Result, - event::{Event, Events}, + event::{BufferedEvent, EntityEvent, Event, Events}, observer::TriggerTargets, resource::Resource, schedule::ScheduleLabel, @@ -144,10 +144,11 @@ where /// A [`Command`] that runs the given system, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached(system: S) -> impl Command +pub fn run_system_cached(system: S) -> impl Command where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached(system)?; @@ -157,11 +158,15 @@ where /// A [`Command`] that runs the given system with the given input value, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached_with(system: S, input: I::Inner<'static>) -> impl Command +pub fn run_system_cached_with( + system: S, + input: I::Inner<'static>, +) -> impl Command where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached_with(system, input)?; @@ -175,7 +180,7 @@ where pub fn unregister_system(system_id: SystemId) -> impl Command where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { move |world: &mut World| -> Result { world.unregister_system(system_id)?; @@ -208,7 +213,7 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } -/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets. +/// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] pub fn trigger(event: impl Event) -> impl Command { let caller = MaybeLocation::caller(); @@ -217,9 +222,10 @@ pub fn trigger(event: impl Event) -> impl Command { } } -/// A [`Command`] that sends a [`Trigger`](crate::observer::Trigger) for the given targets. +/// A [`Command`] that sends an [`EntityEvent`] for the given targets. +#[track_caller] pub fn trigger_targets( - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) -> impl Command { let caller = MaybeLocation::caller(); @@ -228,9 +234,9 @@ pub fn trigger_targets( } } -/// A [`Command`] that sends an arbitrary [`Event`]. +/// A [`Command`] that sends an arbitrary [`BufferedEvent`]. #[track_caller] -pub fn send_event(event: E) -> impl Command { +pub fn send_event(event: E) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { let mut events = world.resource_mut::>(); diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 317ad8476a..87bd2d858b 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -12,7 +12,7 @@ use crate::{ change_detection::MaybeLocation, component::{Component, ComponentId, ComponentInfo}, entity::{Entity, EntityClonerBuilder}, - event::Event, + event::EntityEvent, relationship::RelationshipHookMode, system::IntoObserverSystem, world::{error::EntityMutableFetchError, EntityWorldMut, FromWorld}, @@ -218,7 +218,7 @@ pub fn despawn() -> impl EntityCommand { /// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) /// listening for events of type `E` targeting an entity #[track_caller] -pub fn observe( +pub fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { let caller = MaybeLocation::caller(); @@ -227,11 +227,11 @@ pub fn observe( } } -/// An [`EntityCommand`] that sends a [`Trigger`](crate::observer::Trigger) targeting an entity. +/// An [`EntityCommand`] that sends an [`EntityEvent`] targeting an entity. /// -/// This will run any [`Observer`](crate::observer::Observer) of the given [`Event`] watching the entity. +/// This will run any [`Observer`](crate::observer::Observer) of the given [`EntityEvent`] watching the entity. #[track_caller] -pub fn trigger(event: impl Event) -> impl EntityCommand { +pub fn trigger(event: impl EntityEvent) -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { let id = entity.id(); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 621a9de77e..ee6e01de13 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -16,11 +16,11 @@ use core::marker::PhantomData; use crate::{ self as bevy_ecs, bundle::{Bundle, InsertMode, NoBundleEffect}, - change_detection::Mut, + change_detection::{MaybeLocation, Mut}, component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, - event::Event, + event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, schedule::ScheduleLabel, @@ -88,8 +88,8 @@ use crate::{ /// A [`Command`] can return a [`Result`](crate::error::Result), /// which will be passed to an [error handler](crate::error) if the `Result` is an error. /// -/// The [default error handler](crate::error::default_error_handler) panics. -/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`. +/// The default error handler panics. It can be configured via +/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource. /// /// Alternatively, you can customize the error handler for a specific command /// by calling [`Commands::queue_handled`]. @@ -120,31 +120,26 @@ const _: () = { type Item<'w, 's> = Commands<'w, 's>; - fn init_state( - world: &mut World, - system_meta: &mut bevy_ecs::system::SystemMeta, - ) -> Self::State { + fn init_state(world: &mut World) -> Self::State { FetchState { state: <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_state( world, - system_meta, ), } } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &bevy_ecs::archetype::Archetype, + fn init_access( + state: &Self::State, system_meta: &mut bevy_ecs::system::SystemMeta, + component_access_set: &mut bevy_ecs::query::FilteredAccessSet, + world: &mut World, ) { - // SAFETY: Caller guarantees the archetype is from the world used in `init_state` - unsafe { - <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::new_archetype( - &mut state.state, - archetype, - system_meta, - ); - }; + <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); } fn apply( @@ -173,12 +168,12 @@ const _: () = { #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &bevy_ecs::system::SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { <(Deferred, &Entities) as bevy_ecs::system::SystemParam>::validate_param( - &state.state, + &mut state.state, system_meta, world, ) @@ -317,12 +312,24 @@ impl<'w, 's> Commands<'w, 's> { /// - [`spawn`](Self::spawn) to spawn an entity with components. /// - [`spawn_batch`](Self::spawn_batch) to spawn many entities /// with the same combination of components. + #[track_caller] pub fn spawn_empty(&mut self) -> EntityCommands { let entity = self.entities.reserve_entity(); - EntityCommands { + let mut entity_commands = EntityCommands { entity, commands: self.reborrow(), - } + }; + let caller = MaybeLocation::caller(); + entity_commands.queue(move |entity: EntityWorldMut| { + let index = entity.id().index(); + let world = entity.into_world_mut(); + let tick = world.change_tick(); + // SAFETY: Entity has been flushed + unsafe { + world.entities_mut().mark_spawn_despawn(index, caller, tick); + } + }); + entity_commands } /// Spawns a new [`Entity`] with the given components @@ -369,9 +376,35 @@ impl<'w, 's> Commands<'w, 's> { /// with the same combination of components. #[track_caller] pub fn spawn(&mut self, bundle: T) -> EntityCommands { - let mut entity = self.spawn_empty(); - entity.insert(bundle); - entity + let entity = self.entities.reserve_entity(); + let mut entity_commands = EntityCommands { + entity, + commands: self.reborrow(), + }; + let caller = MaybeLocation::caller(); + + entity_commands.queue(move |mut entity: EntityWorldMut| { + // Store metadata about the spawn operation. + // This is the same as in `spawn_empty`, but merged into + // the same command for better performance. + let index = entity.id().index(); + entity.world_scope(|world| { + let tick = world.change_tick(); + // SAFETY: Entity has been flushed + unsafe { + world.entities_mut().mark_spawn_despawn(index, caller, tick); + } + }); + + entity.insert_with_caller( + bundle, + InsertMode::Replace, + caller, + crate::relationship::RelationshipHookMode::Run, + ); + }); + // entity_command::insert(bundle, InsertMode::Replace) + entity_commands } /// Returns the [`EntityCommands`] for the given [`Entity`]. @@ -508,7 +541,7 @@ impl<'w, 's> Commands<'w, 's> { /// Pushes a generic [`Command`] to the command queue. /// /// If the [`Command`] returns a [`Result`], - /// it will be handled using the [default error handler](crate::error::default_error_handler). + /// it will be handled using the [default error handler](crate::error::DefaultErrorHandler). /// /// To use a custom error handler, see [`Commands::queue_handled`]. /// @@ -643,7 +676,7 @@ impl<'w, 's> Commands<'w, 's> { /// This command will fail if any of the given entities do not exist. /// /// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError), - /// which will be handled by the [default error handler](crate::error::default_error_handler). + /// which will be handled by the [default error handler](crate::error::DefaultErrorHandler). #[track_caller] pub fn insert_batch(&mut self, batch: I) where @@ -674,7 +707,7 @@ impl<'w, 's> Commands<'w, 's> { /// This command will fail if any of the given entities do not exist. /// /// It will internally return a [`TryInsertBatchError`](crate::world::error::TryInsertBatchError), - /// which will be handled by the [default error handler](crate::error::default_error_handler). + /// which will be handled by the [default error handler](crate::error::DefaultErrorHandler). #[track_caller] pub fn insert_batch_if_new(&mut self, batch: I) where @@ -839,7 +872,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// It will internally return a [`RegisteredSystemError`](crate::system::system_registry::RegisteredSystemError), /// which will be handled by [logging the error at the `warn` level](warn). - pub fn run_system(&mut self, id: SystemId) { + pub fn run_system(&mut self, id: SystemId<(), O>) { self.queue(command::run_system(id).handle_error_with(warn)); } @@ -932,7 +965,7 @@ impl<'w, 's> Commands<'w, 's> { ) -> SystemId where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { let entity = self.spawn_empty().id(); let system = RegisteredSystem::::new(Box::new(IntoSystem::into_system(system))); @@ -957,7 +990,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn unregister_system(&mut self, system_id: SystemId) where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { self.queue(command::unregister_system(system_id).handle_error_with(warn)); } @@ -1006,10 +1039,11 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs via [`Commands::run_system_cached_with`]. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached(&mut self, system: S) + pub fn run_system_cached(&mut self, system: S) where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { self.queue(command::run_system_cached(system).handle_error_with(warn)); } @@ -1036,16 +1070,17 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) + pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [`Event`] without any targets. /// /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. #[track_caller] @@ -1053,13 +1088,13 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::trigger(event)); } - /// Sends a [`Trigger`](crate::observer::Trigger) for the given targets. + /// Sends an [`EntityEvent`] for the given targets. /// - /// This will run any [`Observer`] of the given [`Event`] watching those targets. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. #[track_caller] pub fn trigger_targets( &mut self, - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.queue(command::trigger_targets(event, targets)); @@ -1068,6 +1103,8 @@ impl<'w, 's> Commands<'w, 's> { /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated /// with the entity that stores the observer. /// + /// `observer` can be any system whose first parameter is [`On`]. + /// /// **Calling [`observe`](EntityCommands::observe) on the returned /// [`EntityCommands`] will observe the observer itself, which you very /// likely do not want.** @@ -1075,6 +1112,8 @@ impl<'w, 's> Commands<'w, 's> { /// # Panics /// /// Panics if the given system is an exclusive system. + /// + /// [`On`]: crate::observer::On pub fn add_observer( &mut self, observer: impl IntoObserverSystem, @@ -1082,7 +1121,7 @@ impl<'w, 's> Commands<'w, 's> { self.spawn(Observer::new(observer)) } - /// Sends an arbitrary [`Event`]. + /// Sends an arbitrary [`BufferedEvent`]. /// /// This is a convenience method for sending events /// without requiring an [`EventWriter`](crate::event::EventWriter). @@ -1095,7 +1134,7 @@ impl<'w, 's> Commands<'w, 's> { /// If these events are performance-critical or very frequently sent, /// consider using a typed [`EventWriter`](crate::event::EventWriter) instead. #[track_caller] - pub fn send_event(&mut self, event: E) -> &mut Self { + pub fn send_event(&mut self, event: E) -> &mut Self { self.queue(command::send_event(event)); self } @@ -1175,8 +1214,8 @@ impl<'w, 's> Commands<'w, 's> { /// An [`EntityCommand`] can return a [`Result`](crate::error::Result), /// which will be passed to an [error handler](crate::error) if the `Result` is an error. /// -/// The [default error handler](crate::error::default_error_handler) panics. -/// It can be configured by setting the `GLOBAL_ERROR_HANDLER`. +/// The default error handler panics. It can be configured via +/// the [`DefaultErrorHandler`](crate::error::DefaultErrorHandler) resource. /// /// Alternatively, you can customize the error handler for a specific command /// by calling [`EntityCommands::queue_handled`]. @@ -1231,15 +1270,32 @@ impl<'a> EntityCommands<'a> { /// #[derive(Component)] /// struct Level(u32); /// + /// + /// #[derive(Component, Default)] + /// struct Mana { + /// max: u32, + /// current: u32, + /// } + /// /// fn level_up_system(mut commands: Commands, player: Res) { + /// // If a component already exists then modify it, otherwise insert a default value /// commands /// .entity(player.entity) /// .entry::() - /// // Modify the component if it exists. /// .and_modify(|mut lvl| lvl.0 += 1) - /// // Otherwise, insert a default value. /// .or_insert(Level(0)); + /// + /// // Add a default value if none exists, and then modify the existing or new value + /// commands + /// .entity(player.entity) + /// .entry::() + /// .or_default() + /// .and_modify(|mut mana| { + /// mana.max += 10; + /// mana.current = mana.max; + /// }); /// } + /// /// # bevy_ecs::system::assert_is_system(level_up_system); /// ``` pub fn entry(&mut self) -> EntityEntryCommands { @@ -1768,7 +1824,7 @@ impl<'a> EntityCommands<'a> { /// Pushes an [`EntityCommand`] to the queue, /// which will get executed for the current [`Entity`]. /// - /// The [default error handler](crate::error::default_error_handler) + /// The [default error handler](crate::error::DefaultErrorHandler) /// will be used to handle error cases. /// Every [`EntityCommand`] checks whether the entity exists at the time of execution /// and returns an error if it does not. @@ -1903,16 +1959,16 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Sends a [`Trigger`](crate::observer::Trigger) targeting the entity. + /// Sends an [`EntityEvent`] targeting the entity. /// - /// This will run any [`Observer`] of the given [`Event`] watching this entity. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching this entity. #[track_caller] - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.queue(entity_command::trigger(event)) } /// Creates an [`Observer`] listening for events of type `E` targeting this entity. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { @@ -2286,7 +2342,7 @@ mod tests { .spawn((W(1u32), W(2u64))) .id(); command_queue.apply(&mut world); - assert_eq!(world.entities().len(), 1); + assert_eq!(world.entity_count(), 1); let results = world .query::<(&W, &W)>() .iter(&world) @@ -2573,4 +2629,17 @@ mod tests { assert!(world.contains_resource::>()); assert!(world.contains_resource::>()); } + + #[test] + fn track_spawn_ticks() { + let mut world = World::default(); + world.increment_change_tick(); + let expected = world.change_tick(); + let id = world.commands().spawn_empty().id(); + world.flush(); + assert_eq!( + Some(expected), + world.entities().entity_get_spawned_or_despawned_at(id) + ); + } } diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 9107993f95..3a053f89d5 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,7 +1,6 @@ use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + component::{CheckChangeTicks, ComponentId, Tick}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoSystem, @@ -11,10 +10,11 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; -use super::SystemParamValidationError; +use super::{SystemParamValidationError, SystemStateFlags}; /// A function system that runs with exclusive [`World`] access. /// @@ -27,6 +27,8 @@ where F: ExclusiveSystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, param_state: Option<::State>, system_meta: SystemMeta, // NOTE: PhantomData T> gives this safe Send/Sync impls @@ -41,7 +43,7 @@ where /// /// Useful to give closure systems more readable and unique names for debugging and tracing. pub fn with_name(mut self, new_name: impl Into>) -> Self { - self.system_meta.set_name(new_name.into()); + self.system_meta.set_name(new_name); self } } @@ -59,6 +61,11 @@ where fn into_system(func: Self) -> Self::System { ExclusiveFunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current( + >::run, + ) + .ptr_address(), param_state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -77,42 +84,17 @@ where type Out = F::Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - - #[inline] - fn archetype_component_access(&self) -> &Access { - &self.system_meta.archetype_component_access - } - - #[inline] - fn is_send(&self) -> bool { - // exclusive systems should have access to non-send resources + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred // the executor runs exclusive systems on the main thread, so this // field reflects that constraint - false - } - - #[inline] - fn is_exclusive(&self) -> bool { - true - } - - #[inline] - fn has_deferred(&self) -> bool { // exclusive systems have no deferred system params - false + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } #[inline] @@ -131,6 +113,20 @@ where self.param_state.as_mut().expect(PARAM_MESSAGE), &self.system_meta, ); + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = + subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, world, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(world, input, params); world.flush(); @@ -140,6 +136,17 @@ where }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run) + .ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, _world: &mut World) { // "pure" exclusive systems do not have any buffers to apply. @@ -164,19 +171,18 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); self.param_state = Some(F::Param::init(world, &mut self.system_meta)); + FilteredAccessSet::new() } - fn update_archetype_component_access(&mut self, _world: UnsafeWorldCell) {} - #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, - self.system_meta.name.as_ref(), + check, + self.system_meta.name.clone(), ); } diff --git a/crates/bevy_ecs/src/system/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index f271e32e2f..f87182ab1f 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -4,7 +4,7 @@ use crate::{ system::{Local, SystemMeta, SystemParam, SystemState}, world::World, }; -use bevy_utils::synccell::SyncCell; +use bevy_platform::cell::SyncCell; use core::marker::PhantomData; use variadics_please::all_tuples; diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5cf3fe2a44..6009662809 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,8 +1,7 @@ use crate::{ - archetype::{ArchetypeComponentId, ArchetypeGeneration}, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::FromWorld, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ReadOnlySystemParam, System, SystemIn, SystemInput, SystemParam, @@ -12,36 +11,24 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; #[cfg(feature = "trace")] use tracing::{info_span, Span}; -use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError}; +use super::{ + IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError, SystemStateFlags, +}; /// The metadata of a [`System`]. #[derive(Clone)] pub struct SystemMeta { - pub(crate) name: Cow<'static, str>, - /// The set of component accesses for this system. This is used to determine - /// - soundness issues (e.g. multiple [`SystemParam`]s mutably accessing the same component) - /// - ambiguities in the schedule (e.g. two systems that have some sort of conflicting access) - pub(crate) component_access_set: FilteredAccessSet, - /// This [`Access`] is used to determine which systems can run in parallel with each other - /// in the multithreaded executor. - /// - /// We use a [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: - /// for example if you have one system with `Query<&mut T, With>` and one system with `Query<&mut T, With>` - /// they conflict if you just look at the [`ComponentId`] of `T`; but if there are no archetypes with - /// both `A`, `B` and `T` then in practice there's no risk of conflict. By using [`ArchetypeComponentId`] - /// we can be more precise because we can check if the existing archetypes of the [`World`] - /// cause a conflict - pub(crate) archetype_component_access: Access, + pub(crate) name: DebugName, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other - is_send: bool, - has_deferred: bool, + flags: SystemStateFlags, pub(crate) last_run: Tick, #[cfg(feature = "trace")] pub(crate) system_span: Span, @@ -51,24 +38,21 @@ pub struct SystemMeta { impl SystemMeta { pub(crate) fn new() -> Self { - let name = core::any::type_name::(); + let name = DebugName::type_name::(); Self { - name: name.into(), - archetype_component_access: Access::default(), - component_access_set: FilteredAccessSet::default(), - is_send: true, - has_deferred: false, + #[cfg(feature = "trace")] + system_span: info_span!("system", name = name.clone().as_string()), + #[cfg(feature = "trace")] + commands_span: info_span!("system_commands", name = name.clone().as_string()), + name, + flags: SystemStateFlags::empty(), last_run: Tick::new(0), - #[cfg(feature = "trace")] - system_span: info_span!("system", name = name), - #[cfg(feature = "trace")] - commands_span: info_span!("system_commands", name = name), } } /// Returns the system's name #[inline] - pub fn name(&self) -> &str { + pub fn name(&self) -> &DebugName { &self.name } @@ -84,13 +68,13 @@ impl SystemMeta { self.system_span = info_span!("system", name = name); self.commands_span = info_span!("system_commands", name = name); } - self.name = new_name; + self.name = new_name.into(); } /// Returns true if the system is [`Send`]. #[inline] pub fn is_send(&self) -> bool { - self.is_send + !self.flags.intersects(SystemStateFlags::NON_SEND) } /// Sets the system to be not [`Send`]. @@ -98,69 +82,20 @@ impl SystemMeta { /// This is irreversible. #[inline] pub fn set_non_send(&mut self) { - self.is_send = false; + self.flags |= SystemStateFlags::NON_SEND; } /// Returns true if the system has deferred [`SystemParam`]'s #[inline] pub fn has_deferred(&self) -> bool { - self.has_deferred + self.flags.intersects(SystemStateFlags::DEFERRED) } /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`) /// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically. #[inline] pub fn set_has_deferred(&mut self) { - self.has_deferred = true; - } - - /// Archetype component access that is used to determine which systems can run in parallel with each other - /// in the multithreaded executor. - /// - /// We use an [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: - /// for example if you have one system with `Query<&mut A, With`, and one system with `Query<&mut A, Without`, - /// they conflict if you just look at the [`ComponentId`]; - /// but no archetype that matches the first query will match the second and vice versa, - /// which means there's no risk of conflict. - #[inline] - pub fn archetype_component_access(&self) -> &Access { - &self.archetype_component_access - } - - /// Returns a mutable reference to the [`Access`] for [`ArchetypeComponentId`]. - /// This is used to determine which systems can run in parallel with each other - /// in the multithreaded executor. - /// - /// We use an [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: - /// for example if you have one system with `Query<&mut A, With`, and one system with `Query<&mut A, Without`, - /// they conflict if you just look at the [`ComponentId`]; - /// but no archetype that matches the first query will match the second and vice versa, - /// which means there's no risk of conflict. - /// - /// # Safety - /// - /// No access can be removed from the returned [`Access`]. - #[inline] - pub unsafe fn archetype_component_access_mut(&mut self) -> &mut Access { - &mut self.archetype_component_access - } - - /// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`]. - /// Used to check if systems and/or system params have conflicting access. - #[inline] - pub fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - /// Returns a mutable reference to the [`FilteredAccessSet`] for [`ComponentId`]. - /// Used internally to statically check if systems have conflicting access. - /// - /// # Safety - /// - /// No access can be removed from the returned [`FilteredAccessSet`]. - #[inline] - pub unsafe fn component_access_set_mut(&mut self) -> &mut FilteredAccessSet { - &mut self.component_access_set + self.flags |= SystemStateFlags::DEFERRED; } } @@ -197,7 +132,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # #[derive(Resource)] /// # struct MyResource(u32); @@ -230,7 +165,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// #[derive(Resource)] /// struct CachedSystemState { @@ -260,7 +195,6 @@ pub struct SystemState { meta: SystemMeta, param_state: Param::State, world_id: WorldId, - archetype_generation: ArchetypeGeneration, } // Allow closure arguments to be inferred. @@ -318,21 +252,18 @@ all_tuples!( impl SystemState { /// Creates a new [`SystemState`] with default state. - /// - /// ## Note - /// For users of [`SystemState::get_manual`] or [`get_manual_mut`](SystemState::get_manual_mut): - /// - /// `new` does not cache any of the world's archetypes, so you must call [`SystemState::update_archetypes`] - /// manually before calling `get_manual{_mut}`. pub fn new(world: &mut World) -> Self { let mut meta = SystemMeta::new::(); meta.last_run = world.change_tick().relative_to(Tick::MAX); - let param_state = Param::init_state(world, &mut meta); + let param_state = Param::init_state(world); + let mut component_access_set = FilteredAccessSet::new(); + // We need to call `init_access` to ensure there are no panics from conflicts within `Param`, + // even though we don't use the calculated access. + Param::init_access(¶m_state, &mut meta, &mut component_access_set, world); Self { meta, param_state, world_id: world.id(), - archetype_generation: ArchetypeGeneration::initial(), } } @@ -340,12 +271,15 @@ impl SystemState { pub(crate) fn from_builder(world: &mut World, builder: impl SystemParamBuilder) -> Self { let mut meta = SystemMeta::new::(); meta.last_run = world.change_tick().relative_to(Tick::MAX); - let param_state = builder.build(world, &mut meta); + let param_state = builder.build(world); + let mut component_access_set = FilteredAccessSet::new(); + // We need to call `init_access` to ensure there are no panics from conflicts within `Param`, + // even though we don't use the calculated access. + Param::init_access(¶m_state, &mut meta, &mut component_access_set, world); Self { meta, param_state, world_id: world.id(), - archetype_generation: ArchetypeGeneration::initial(), } } @@ -358,12 +292,14 @@ impl SystemState { ) -> FunctionSystem { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: Some(FunctionSystemState { param: self.param_state, world_id: self.world_id, }), system_meta: self.meta, - archetype_generation: self.archetype_generation, marker: PhantomData, } } @@ -387,19 +323,17 @@ impl SystemState { Param: ReadOnlySystemParam, { self.validate_world(world.id()); - self.update_archetypes(world); // SAFETY: Param is read-only and doesn't allow mutable access to World. // It also matches the World this SystemState was created with. - unsafe { self.get_unchecked_manual(world.as_unsafe_world_cell_readonly()) } + unsafe { self.get_unchecked(world.as_unsafe_world_cell_readonly()) } } /// Retrieve the mutable [`SystemParam`] values. #[inline] pub fn get_mut<'w, 's>(&'s mut self, world: &'w mut World) -> SystemParamItem<'w, 's, Param> { self.validate_world(world.id()); - self.update_archetypes(world); // SAFETY: World is uniquely borrowed and matches the World this SystemState was created with. - unsafe { self.get_unchecked_manual(world.as_unsafe_world_cell()) } + unsafe { self.get_unchecked(world.as_unsafe_world_cell()) } } /// Applies all state queued up for [`SystemParam`] values. For example, this will apply commands queued up @@ -415,14 +349,14 @@ impl SystemState { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have read-only access to - /// world data in `archetype_component_access`. + /// world data in `component_access_set`. /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). pub unsafe fn validate_param( - state: &Self, + state: &mut Self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { // SAFETY: Delegated to existing `SystemParam` implementations. - unsafe { Param::validate_param(&state.param_state, &state.meta, world) } + unsafe { Param::validate_param(&mut state.param_state, &state.meta, world) } } /// Returns `true` if `world_id` matches the [`World`] that was used to call [`SystemState::new`]. @@ -448,89 +382,68 @@ impl SystemState { } } - /// Updates the state's internal view of the [`World`]'s archetypes. If this is not called before fetching the parameters, - /// the results may not accurately reflect what is in the `world`. - /// - /// This is only required if [`SystemState::get_manual`] or [`SystemState::get_manual_mut`] is being called, and it only needs to - /// be called if the `world` has been structurally mutated (i.e. added/removed a component or resource). Users using - /// [`SystemState::get`] or [`SystemState::get_mut`] do not need to call this as it will be automatically called for them. + /// Has no effect #[inline] - pub fn update_archetypes(&mut self, world: &World) { - self.update_archetypes_unsafe_world_cell(world.as_unsafe_world_cell_readonly()); - } + #[deprecated( + since = "0.17.0", + note = "No longer has any effect. Calls may be removed." + )] + pub fn update_archetypes(&mut self, _world: &World) {} - /// Updates the state's internal view of the `world`'s archetypes. If this is not called before fetching the parameters, - /// the results may not accurately reflect what is in the `world`. - /// - /// This is only required if [`SystemState::get_manual`] or [`SystemState::get_manual_mut`] is being called, and it only needs to - /// be called if the `world` has been structurally mutated (i.e. added/removed a component or resource). Users using - /// [`SystemState::get`] or [`SystemState::get_mut`] do not need to call this as it will be automatically called for them. - /// - /// # Note - /// - /// This method only accesses world metadata. + /// Has no effect #[inline] - pub fn update_archetypes_unsafe_world_cell(&mut self, world: UnsafeWorldCell) { - assert_eq!(self.world_id, world.id(), "Encountered a mismatched World. A System cannot be used with Worlds other than the one it was initialized with."); + #[deprecated( + since = "0.17.0", + note = "No longer has any effect. Calls may be removed." + )] + pub fn update_archetypes_unsafe_world_cell(&mut self, _world: UnsafeWorldCell) {} - let archetypes = world.archetypes(); - let old_generation = - core::mem::replace(&mut self.archetype_generation, archetypes.generation()); - - for archetype in &archetypes[old_generation..] { - // SAFETY: The assertion above ensures that the param_state was initialized from `world`. - unsafe { Param::new_archetype(&mut self.param_state, archetype, &mut self.meta) }; - } - } - - /// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only. - /// This will not update the state's view of the world's archetypes automatically nor increment the - /// world's change tick. - /// - /// For this to return accurate results, ensure [`SystemState::update_archetypes`] is called before this - /// function. - /// - /// Users should strongly prefer to use [`SystemState::get`] over this function. + /// Identical to [`SystemState::get`]. #[inline] + #[deprecated(since = "0.17.0", note = "Call `SystemState::get` instead.")] pub fn get_manual<'w, 's>(&'s mut self, world: &'w World) -> SystemParamItem<'w, 's, Param> where Param: ReadOnlySystemParam, { - self.validate_world(world.id()); - let change_tick = world.read_change_tick(); - // SAFETY: Param is read-only and doesn't allow mutable access to World. - // It also matches the World this SystemState was created with. - unsafe { self.fetch(world.as_unsafe_world_cell_readonly(), change_tick) } + self.get(world) } - /// Retrieve the mutable [`SystemParam`] values. This will not update the state's view of the world's archetypes - /// automatically nor increment the world's change tick. - /// - /// For this to return accurate results, ensure [`SystemState::update_archetypes`] is called before this - /// function. - /// - /// Users should strongly prefer to use [`SystemState::get_mut`] over this function. + /// Identical to [`SystemState::get_mut`]. #[inline] + #[deprecated(since = "0.17.0", note = "Call `SystemState::get_mut` instead.")] pub fn get_manual_mut<'w, 's>( &'s mut self, world: &'w mut World, ) -> SystemParamItem<'w, 's, Param> { - self.validate_world(world.id()); - let change_tick = world.change_tick(); - // SAFETY: World is uniquely borrowed and matches the World this SystemState was created with. - unsafe { self.fetch(world.as_unsafe_world_cell(), change_tick) } + self.get_mut(world) } - /// Retrieve the [`SystemParam`] values. This will not update archetypes automatically. + /// Identical to [`SystemState::get_unchecked`]. /// /// # Safety /// This call might access any of the input parameters in a way that violates Rust's mutability rules. Make sure the data /// access is safe in the context of global [`World`] access. The passed-in [`World`] _must_ be the [`World`] the [`SystemState`] was /// created with. #[inline] + #[deprecated(since = "0.17.0", note = "Call `SystemState::get_unchecked` instead.")] pub unsafe fn get_unchecked_manual<'w, 's>( &'s mut self, world: UnsafeWorldCell<'w>, + ) -> SystemParamItem<'w, 's, Param> { + // SAFETY: Caller ensures safety requirements + unsafe { self.get_unchecked(world) } + } + + /// Retrieve the [`SystemParam`] values. + /// + /// # Safety + /// This call might access any of the input parameters in a way that violates Rust's mutability rules. Make sure the data + /// access is safe in the context of global [`World`] access. The passed-in [`World`] _must_ be the [`World`] the [`SystemState`] was + /// created with. + #[inline] + pub unsafe fn get_unchecked<'w, 's>( + &'s mut self, + world: UnsafeWorldCell<'w>, ) -> SystemParamItem<'w, 's, Param> { let change_tick = world.increment_change_tick(); // SAFETY: The invariants are upheld by the caller. @@ -567,8 +480,7 @@ impl SystemState { /// Modifying the system param states may have unintended consequences. /// The param state is generally considered to be owned by the [`SystemParam`]. Modifications /// should respect any invariants as required by the [`SystemParam`]. - /// For example, modifying the system state of [`ResMut`](crate::system::ResMut) without also - /// updating [`SystemMeta::component_access_set`] will obviously create issues. + /// For example, modifying the system state of [`ResMut`](crate::system::ResMut) will obviously create issues. pub unsafe fn param_state_mut(&mut self) -> &mut Param::State { &mut self.param_state } @@ -595,9 +507,10 @@ where F: SystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, - archetype_generation: ArchetypeGeneration, // NOTE: PhantomData T> gives this safe Send/Sync impls marker: PhantomData Marker>, } @@ -609,7 +522,7 @@ 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 + /// passed to [`System::run_unsafe`] or [`System::validate_param_unsafe`] does not match /// this id, a panic will occur. world_id: WorldId, } @@ -635,9 +548,11 @@ where fn clone(&self) -> Self { Self { func: self.func.clone(), + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), - archetype_generation: ArchetypeGeneration::initial(), marker: PhantomData, } } @@ -656,9 +571,11 @@ where fn into_system(func: Self) -> Self::System { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), - archetype_generation: ArchetypeGeneration::initial(), marker: PhantomData, } } @@ -684,38 +601,13 @@ where type Out = F::Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - - #[inline] - fn archetype_component_access(&self) -> &Access { - &self.system_meta.archetype_component_access - } - - #[inline] - fn is_send(&self) -> bool { - self.system_meta.is_send - } - - #[inline] - fn is_exclusive(&self) -> bool { - false - } - - #[inline] - fn has_deferred(&self) -> bool { - self.system_meta.has_deferred + fn flags(&self) -> SystemStateFlags { + self.system_meta.flags } #[inline] @@ -729,19 +621,43 @@ where let change_tick = world.increment_change_tick(); - let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; + 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."); // SAFETY: - // - The caller has invoked `update_archetype_component_access`, which will panic - // if the world does not match. + // - The above assert ensures the world matches. // - 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(param_state, &self.system_meta, world, change_tick) }; + unsafe { F::Param::get_param(&mut state.param, &self.system_meta, world, change_tick) }; + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(input, params); + self.system_meta.last_run = change_tick; out } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run).ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; @@ -759,52 +675,45 @@ where &mut self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - let param_state = &self.state.as_ref().expect(Self::ERROR_UNINITIALIZED).param; + 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."); // SAFETY: - // - The caller has invoked `update_archetype_component_access`, which will panic - // if the world does not match. + // - The above assert ensures the world matches. // - All world accesses used by `F::Param` have been registered, so the caller // will ensure that there are no data access conflicts. - unsafe { F::Param::validate_param(param_state, &self.system_meta, world) } + unsafe { F::Param::validate_param(&mut state.param, &self.system_meta, world) } } #[inline] - fn initialize(&mut self, world: &mut World) { + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { if let Some(state) = &self.state { assert_eq!( state.world_id, world.id(), "System built with a different world than the one it was added to.", ); - } else { - self.state = Some(FunctionSystemState { - param: F::Param::init_state(world, &mut self.system_meta), - world_id: world.id(), - }); } + let state = self.state.get_or_insert_with(|| FunctionSystemState { + param: F::Param::init_state(world), + 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) { - 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..] { - // SAFETY: The assertion above ensures that the param_state was initialized from `world`. - unsafe { F::Param::new_archetype(&mut state.param, archetype, &mut self.system_meta) }; - } + let mut component_access_set = FilteredAccessSet::new(); + F::Param::init_access( + &state.param, + &mut self.system_meta, + &mut component_access_set, + world, + ); + component_access_set } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, - self.system_meta.name.as_ref(), + check, + self.system_meta.name.clone(), ); } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 12087fdf6a..cb75016ee9 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, DerefMut}; use variadics_please::all_tuples; -use crate::{bundle::Bundle, prelude::Trigger, system::System}; +use crate::{bundle::Bundle, prelude::On, system::System}; /// Trait for types that can be used as input to [`System`]s. /// @@ -11,7 +11,7 @@ use crate::{bundle::Bundle, prelude::Trigger, system::System}; /// - [`In`]: For values /// - [`InRef`]: For read-only references to values /// - [`InMut`]: For mutable references to values -/// - [`Trigger`]: For [`ObserverSystem`]s +/// - [`On`]: For [`ObserverSystem`]s /// - [`StaticSystemInput`]: For arbitrary [`SystemInput`]s in generic contexts /// - Tuples of [`SystemInput`]s up to 8 elements /// @@ -222,9 +222,9 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for Trigger<'_, E, B> { - type Param<'i> = Trigger<'i, E, B>; - type Inner<'i> = Trigger<'i, E, B>; +impl SystemInput for On<'_, E, B> { + type Param<'i> = On<'i, E, B>; + type Inner<'i> = On<'i, E, B>; fn wrap(this: Self::Inner<'_>) -> Self::Param<'_> { this diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 011c220c85..3e63dcf3d6 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -97,7 +97,7 @@ //! - [`EventWriter`](crate::event::EventWriter) //! - [`NonSend`] and `Option` //! - [`NonSendMut`] and `Option` -//! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) +//! - [`RemovedComponents`](crate::lifecycle::RemovedComponents) //! - [`SystemName`] //! - [`SystemChangeTick`] //! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata) @@ -402,26 +402,26 @@ mod tests { use std::println; use crate::{ - archetype::{ArchetypeComponentId, Archetypes}, + archetype::Archetypes, bundle::Bundles, change_detection::DetectChanges, component::{Component, Components}, entity::{Entities, Entity}, error::Result, + lifecycle::RemovedComponents, name::Name, - prelude::{AnyOf, EntityRef, Trigger}, + prelude::{Add, AnyOf, EntityRef, On}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, - removal_detection::RemovedComponents, resource::Resource, schedule::{ - common_conditions::resource_exists, ApplyDeferred, Condition, IntoScheduleConfigs, - Schedule, + common_conditions::resource_exists, ApplyDeferred, IntoScheduleConfigs, Schedule, + SystemCondition, }, system::{ Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut, Single, StaticSystemParam, System, SystemState, }, - world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World}, + world::{DeferredWorld, EntityMut, FromWorld, World}, }; use super::ScheduleSystem; @@ -634,7 +634,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_ref() { fn sys(_: Query>) {} let mut world = World::default(); @@ -642,7 +642,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_ref_and_mut() { fn sys(_: Query>) {} let mut world = World::default(); @@ -650,7 +650,7 @@ mod tests { } #[test] - #[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_mut_and_option() { fn sys(_: Query)>>) {} let mut world = World::default(); @@ -680,7 +680,7 @@ mod tests { } #[test] - #[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."] + #[should_panic] fn any_of_with_conflicting() { fn sys(_: Query>) {} let mut world = World::default(); @@ -1163,10 +1163,10 @@ mod tests { let mut world = World::default(); let mut x = IntoSystem::into_system(sys_x); let mut y = IntoSystem::into_system(sys_y); - x.initialize(&mut world); - y.initialize(&mut world); + let x_access = x.initialize(&mut world); + let y_access = y.initialize(&mut world); - let conflicts = x.component_access().get_conflicts(y.component_access()); + let conflicts = x_access.get_conflicts(&y_access); let b_id = world .components() .get_resource_id(TypeId::of::()) @@ -1587,68 +1587,6 @@ mod tests { } } - #[test] - fn update_archetype_component_access_works() { - use std::collections::HashSet; - - fn a_not_b_system(_query: Query<&A, Without>) {} - - let mut world = World::default(); - let mut system = IntoSystem::into_system(a_not_b_system); - let mut expected_ids = HashSet::::new(); - let a_id = world.register_component::(); - - // set up system and verify its access is empty - system.initialize(&mut world); - system.update_archetype_component_access(world.as_unsafe_world_cell()); - let archetype_component_access = system.archetype_component_access(); - assert!(expected_ids - .iter() - .all(|id| archetype_component_access.has_component_read(*id))); - - // add some entities with archetypes that should match and save their ids - expected_ids.insert( - world - .spawn(A) - .archetype() - .get_archetype_component_id(a_id) - .unwrap(), - ); - expected_ids.insert( - world - .spawn((A, C)) - .archetype() - .get_archetype_component_id(a_id) - .unwrap(), - ); - - // add some entities with archetypes that should not match - world.spawn((A, B)); - world.spawn((B, C)); - - // update system and verify its accesses are correct - system.update_archetype_component_access(world.as_unsafe_world_cell()); - let archetype_component_access = system.archetype_component_access(); - assert!(expected_ids - .iter() - .all(|id| archetype_component_access.has_component_read(*id))); - - // one more round - expected_ids.insert( - world - .spawn((A, D)) - .archetype() - .get_archetype_component_id(a_id) - .unwrap(), - ); - world.spawn((A, B, D)); - system.update_archetype_component_access(world.as_unsafe_world_cell()); - let archetype_component_access = system.archetype_component_access(); - assert!(expected_ids - .iter() - .all(|id| archetype_component_access.has_component_read(*id))); - } - #[test] fn commands_param_set() { // Regression test for #4676 @@ -1691,54 +1629,42 @@ mod tests { } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_first() { fn system(_query: &World, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules" - )] + #[should_panic] fn assert_world_and_entity_mut_system_does_conflict_second() { fn system(_: Query, _: &World) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_ref_and_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" - )] + #[should_panic] fn assert_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_first() { fn system(_world: DeferredWorld, _query: Query) {} super::assert_system_does_not_conflict(system); } #[test] - #[should_panic( - expected = "DeferredWorld in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_second::system conflicts with a previous access." - )] + #[should_panic] fn assert_deferred_world_and_entity_ref_system_does_conflict_second() { fn system(_query: Query, _world: DeferredWorld) {} super::assert_system_does_not_conflict(system); @@ -1962,15 +1888,15 @@ mod tests { #[expect(clippy::unused_unit, reason = "this forces the () return type")] schedule.add_systems(|_query: Query<&Name>| -> () { todo!() }); - fn obs(_trigger: Trigger) { + fn obs(_trigger: On) { todo!() } world.add_observer(obs); - world.add_observer(|_trigger: Trigger| {}); - world.add_observer(|_trigger: Trigger| todo!()); + world.add_observer(|_trigger: On| {}); + world.add_observer(|_trigger: On| todo!()); #[expect(clippy::unused_unit, reason = "this forces the () return type")] - world.add_observer(|_trigger: Trigger| -> () { todo!() }); + world.add_observer(|_trigger: On| -> () { todo!() }); fn my_command(_world: &mut World) { todo!() diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index 9bd35c5361..243c2c3c3f 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -1,13 +1,13 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, never::Never, - prelude::{Bundle, Trigger}, - query::{Access, FilteredAccessSet}, + prelude::{Bundle, On}, + query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -15,14 +15,14 @@ use crate::{ use super::{IntoSystem, SystemParamValidationError}; -/// Implemented for [`System`]s that have a [`Trigger`] as the first argument. +/// Implemented for [`System`]s that have [`On`] as the first argument. pub trait ObserverSystem: - System, Out = Out> + Send + 'static + System, Out = Out> + Send + 'static { } impl ObserverSystem for T where - T: System, Out = Out> + Send + 'static + T: System, Out = Out> + Send + 'static { } @@ -36,7 +36,7 @@ impl ObserverSystem for T where #[diagnostic::on_unimplemented( message = "`{Self}` cannot become an `ObserverSystem`", label = "the trait `IntoObserverSystem` is not implemented", - note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger` and any subsequent ones are `SystemParam`" + note = "for function `ObserverSystem`s, ensure the first argument is `On` and any subsequent ones are `SystemParam`" )] pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. @@ -48,7 +48,7 @@ pub trait IntoObserverSystem: Send + 'st impl IntoObserverSystem for S where - S: IntoSystem, Out, M> + Send + 'static, + S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, B: Bundle, @@ -62,7 +62,7 @@ where impl IntoObserverSystem for S where - S: IntoSystem, (), M> + Send + 'static, + S: IntoSystem, (), M> + Send + 'static, S::System: ObserverSystem, E: Send + Sync + 'static, B: Bundle, @@ -73,9 +73,10 @@ where InfallibleObserverWrapper::new(IntoSystem::into_system(this)) } } + impl IntoObserverSystem for S where - S: IntoSystem, Never, M> + Send + 'static, + S: IntoSystem, Never, M> + Send + 'static, E: Send + Sync + 'static, B: Bundle, { @@ -109,42 +110,17 @@ where B: Bundle, Out: Send + Sync + 'static, { - type In = Trigger<'static, E, B>; + type In = On<'static, E, B>; type Out = Result; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.observer.name() } #[inline] - fn component_access(&self) -> &Access { - self.observer.component_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.observer.component_access_set() - } - - #[inline] - fn archetype_component_access(&self) -> &Access { - self.observer.archetype_component_access() - } - - #[inline] - fn is_send(&self) -> bool { - self.observer.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.observer.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.observer.has_deferred() + fn flags(&self) -> super::SystemStateFlags { + self.observer.flags() } #[inline] @@ -157,6 +133,12 @@ where Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.observer.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.observer.apply_deferred(world); @@ -176,18 +158,13 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { - self.observer.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.observer.initialize(world) } #[inline] - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.observer.update_archetype_component_access(world); - } - - #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.observer.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.observer.check_change_tick(check); } #[inline] @@ -209,7 +186,7 @@ where mod tests { use crate::{ event::Event, - observer::Trigger, + observer::On, system::{In, IntoSystem}, world::World, }; @@ -219,7 +196,7 @@ mod tests { #[test] fn test_piped_observer_systems_no_input() { - fn a(_: Trigger) {} + fn a(_: On) {} fn b() {} let mut world = World::new(); @@ -228,7 +205,7 @@ mod tests { #[test] fn test_piped_observer_systems_with_inputs() { - fn a(_: Trigger) -> u32 { + fn a(_: On) -> u32 { 3 } fn b(_: In) {} diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index c1d9e671a0..6e44301b18 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1,3 +1,5 @@ +use bevy_utils::prelude::DebugName; + use crate::{ batching::BatchingStrategy, component::Tick, @@ -115,7 +117,7 @@ use core::{ /// ``` /// /// Note that the filter is `With`, not `With<&ComponentB>`. Unlike query data, `With` -/// does require components to be behind a reference. +/// does not require components to be behind a reference. /// /// ## `QueryData` or `QueryFilter` tuples /// @@ -209,7 +211,7 @@ use core::{ /// # #[derive(Component)] /// # struct ComponentB; /// # -/// // A queried items must contain `ComponentA`. If they also contain `ComponentB`, its value will +/// // Queried items must contain `ComponentA`. If they also contain `ComponentB`, its value will /// // be fetched as well. /// fn optional_component_query(query: Query<(&ComponentA, Option<&ComponentB>)>) { /// // ... @@ -1185,7 +1187,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter_mut`]: Self::par_iter_mut /// [`World`]: crate::world::World #[inline] - pub fn par_iter(&self) -> QueryParIter<'_, '_, D::ReadOnly, F> { + pub fn par_iter(&self) -> QueryParIter<'_, 's, D::ReadOnly, F> { self.as_readonly().par_iter_inner() } @@ -1220,7 +1222,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter`]: Self::par_iter /// [`World`]: crate::world::World #[inline] - pub fn par_iter_mut(&mut self) -> QueryParIter<'_, '_, D, F> { + pub fn par_iter_mut(&mut self) -> QueryParIter<'_, 's, D, F> { self.reborrow().par_iter_inner() } @@ -1280,7 +1282,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many>( &self, entities: EntityList, - ) -> QueryParManyIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyIter { world: self.world, state: self.state.as_readonly(), @@ -1309,7 +1311,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique>( &self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state.as_readonly(), @@ -1338,7 +1340,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique_mut>( &mut self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state, @@ -1383,7 +1385,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get a mutable query item. #[inline] - pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { + pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { self.as_readonly().get_inner(entity) } @@ -1434,7 +1436,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many( &self, entities: [Entity; N], - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { // Note that we call a separate `*_inner` method from `get_many_mut` // because we don't need to check for duplicates. self.as_readonly().get_many_inner(entities) @@ -1485,7 +1487,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique( &self, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { self.as_readonly().get_many_unique_inner(entities) } @@ -1519,7 +1521,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get`](Self::get) to get a read-only query item. #[inline] - pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { self.reborrow().get_inner(entity) } @@ -1534,7 +1536,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get the item using a mutable borrow of the [`Query`]. #[inline] - pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { @@ -1580,8 +1582,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { D::set_archetype(&mut fetch, &self.state.fetch_state, archetype, table); F::set_archetype(&mut filter, &self.state.filter_state, archetype, table); - if F::filter_fetch(&mut filter, entity, location.table_row) { - Ok(D::fetch(&mut fetch, entity, location.table_row)) + if F::filter_fetch( + &self.state.filter_state, + &mut filter, + entity, + location.table_row, + ) { + Ok(D::fetch( + &self.state.fetch_state, + &mut fetch, + entity, + location.table_row, + )) } else { Err(QueryEntityError::QueryDoesNotMatch( entity, @@ -1662,7 +1674,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut( &mut self, entities: [Entity; N], - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_mut_inner(entities) } @@ -1730,7 +1742,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_mut( &mut self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_unique_inner(entities) } @@ -1749,7 +1761,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // Verify that all entities are unique for i in 0..N { for j in 0..i { @@ -1777,7 +1789,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> where D: ReadOnlyQueryData, { @@ -1799,7 +1811,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_inner( self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // SAFETY: All entities are unique, so the results don't alias. unsafe { self.get_many_impl(entities.into_inner()) } } @@ -1814,7 +1826,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { unsafe fn get_many_impl( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { let mut values = [(); N].map(|_| MaybeUninit::uninit()); for (value, entity) in core::iter::zip(&mut values, entities) { @@ -1842,7 +1854,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) for the safe version. #[inline] - pub unsafe fn get_unchecked(&self, entity: Entity) -> Result, QueryEntityError> { + pub unsafe fn get_unchecked( + &self, + entity: Entity, + ) -> Result, QueryEntityError> { // SAFETY: The caller promises that this will not result in multiple mutable references. unsafe { self.reborrow_unsafe() }.get_inner(entity) } @@ -1878,7 +1893,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single_mut`](Self::single_mut) to get the mutable query item. #[inline] - pub fn single(&self) -> Result, QuerySingleError> { + pub fn single(&self) -> Result, QuerySingleError> { self.as_readonly().single_inner() } @@ -1907,7 +1922,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single`](Self::single) to get the read-only query item. #[inline] - pub fn single_mut(&mut self) -> Result, QuerySingleError> { + pub fn single_mut(&mut self) -> Result, QuerySingleError> { self.reborrow().single_inner() } @@ -1939,15 +1954,15 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// - [`single_mut`](Self::single_mut) to get the mutable query item. /// - [`single_inner`](Self::single_inner) for the panicking version. #[inline] - pub fn single_inner(self) -> Result, QuerySingleError> { + pub fn single_inner(self) -> Result, QuerySingleError> { let mut query = self.into_iter(); let first = query.next(); let extra = query.next().is_some(); match (first, extra) { (Some(r), false) => Ok(r), - (None, _) => Err(QuerySingleError::NoEntities(core::any::type_name::())), - (Some(_), _) => Err(QuerySingleError::MultipleEntities(core::any::type_name::< + (None, _) => Err(QuerySingleError::NoEntities(DebugName::type_name::())), + (Some(_), _) => Err(QuerySingleError::MultipleEntities(DebugName::type_name::< Self, >())), } @@ -2016,17 +2031,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_nop().get(entity).is_ok() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new [`Query`] giving more + /// restrictive access to the entities matched by the current query. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// A transmute is valid only if `NewD` has a subset of the read, write, and required access + /// of the current query. A precise description of the access required by each parameter + /// type is given in the table below, but typical uses are to: + /// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`. + /// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>` + /// or `Query<&T>` to `Query>`. + /// * Add parameters with no new access, for example adding an `Entity` parameter. + /// + /// Note that since filter terms are dropped, non-archetypal filters like + /// [`Added`], [`Changed`] and [`Spawned`] will not be respected. To maintain or change filter + /// terms see [`Self::transmute_lens_filtered`]. + /// + /// |`QueryData` parameter type|Access required| + /// |----|----| + /// |[`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], [`PhantomData`]|No access| + /// |[`EntityMut`]|Read and write access to all components, but no required access| + /// |[`EntityRef`]|Read access to all components, but no required access| + /// |`&T`, [`Ref`]|Read and required access to `T`| + /// |`&mut T`, [`Mut`]|Read, write and required access to `T`| + /// |[`Option`], [`AnyOf<(D, ...)>`]|Read and write access to `T`, but no required access| + /// |Tuples of query data and
`#[derive(QueryData)]` structs|The union of the access of their subqueries| + /// |[`FilteredEntityRef`], [`FilteredEntityMut`]|Determined by the [`QueryBuilder`] used to construct them. Any query can be transmuted to them, and they will receive the access of the source query. When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data| + /// + /// `transmute_lens` drops filter terms, but [`Self::transmute_lens_filtered`] supports returning a [`QueryLens`] with a new + /// filter type - the access required by filter parameters are as follows. + /// + /// |`QueryFilter` parameter type|Access required| + /// |----|----| + /// |[`Added`], [`Changed`]|Read and required access to `T`| + /// |[`With`], [`Without`]|No access| + /// |[`Or<(T, ...)>`]|Read access of the subqueries, but no required access| + /// |Tuples of query filters and `#[derive(QueryFilter)]` structs|The union of the access of their subqueries| + /// + /// [`Added`]: crate::query::Added + /// [`Added`]: crate::query::Added + /// [`AnyOf<(D, ...)>`]: crate::query::AnyOf + /// [`&Archetype`]: crate::archetype::Archetype + /// [`Changed`]: crate::query::Changed + /// [`Changed`]: crate::query::Changed + /// [`EntityMut`]: crate::world::EntityMut + /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`EntityRef`]: crate::world::EntityRef + /// [`FilteredEntityRef`]: crate::world::FilteredEntityRef + /// [`FilteredEntityMut`]: crate::world::FilteredEntityMut + /// [`Has`]: crate::query::Has + /// [`Mut`]: crate::world::Mut + /// [`Or<(T, ...)>`]: crate::query::Or + /// [`QueryBuilder`]: crate::query::QueryBuilder + /// [`Ref`]: crate::world::Ref + /// [`SpawnDetails`]: crate::query::SpawnDetails + /// [`Spawned`]: crate::query::Spawned + /// [`With`]: crate::query::With + /// [`Without`]: crate::query::Without /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `D` + /// This will panic if the access required by `NewD` is not a subset of that required by + /// the original fetch `D`. /// /// ## Example /// @@ -2065,30 +2130,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, - /// you can also make limited changes to the types of parameters. - /// The new query must have a subset of the *read*, *write*, and *required* access of the original query. - /// - /// * `&mut T` and [`Mut`](crate::change_detection::Mut) have read, write, and required access to `T` - /// * `&T` and [`Ref`](crate::change_detection::Ref) have read and required access to `T` - /// * [`Option`] and [`AnyOf<(D, ...)>`](crate::query::AnyOf) have the read and write access of the subqueries, but no required access - /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries - /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access - /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, - /// so can be added to any query - /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) - /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. - /// Any query can be transmuted to them, and they will receive the access of the source query. - /// When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data. - /// * [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) filters have read and required access to `T` - /// * [`With`](crate::query::With) and [`Without`](crate::query::Without) filters have no access at all, - /// so can be added to any query - /// * Tuples of query filters and `#[derive(QueryFilter)]` structs have the union of the access of their subqueries - /// * [`Or<(F, ...)>`](crate::query::Or) filters have the read access of the subqueries, but no required access - /// /// ### Examples of valid transmutes /// /// ```rust @@ -2165,28 +2206,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`. /// assert_valid_transmute_filtered::, (), Entity, Or<(Changed, With)>>(); /// ``` - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`SpawnDetails`]: crate::query::SpawnDetails - /// [`&Archetype`]: crate::archetype::Archetype - /// [`Has`]: crate::query::Has #[track_caller] pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { self.transmute_lens_filtered::() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new `Query` giving more restrictive + /// access to the entities matched by the current query. + /// /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// See [`Self::transmute_lens`] for a description of allowed transmutes. /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `Q` + /// This will panic if `NewD` is not a subset of the original fetch `D` /// /// ## Example /// @@ -2225,22 +2259,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, you can also - /// make limited changes to the types of parameters. - /// - /// * Can always add/remove [`Entity`] - /// * Can always add/remove [`EntityLocation`] - /// * Can always add/remove [`&Archetype`] - /// * `Ref` <-> `&T` - /// * `&mut T` -> `&T` - /// * `&mut T` -> `Ref` - /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`&Archetype`]: crate::archetype::Archetype - /// /// # See also /// /// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`]. @@ -2251,6 +2269,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), @@ -2266,10 +2286,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens_inner`] but also includes a [`QueryFilter`] type. /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. /// /// # See also /// @@ -2443,7 +2466,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2456,7 +2479,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, F> { - type Item = ROQueryItem<'w, D>; + type Item = ROQueryItem<'w, 's, D>; type IntoIter = QueryIter<'w, 's, D::ReadOnly, F>; fn into_iter(self) -> Self::IntoIter { @@ -2465,7 +2488,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w mut Query<'_, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2565,28 +2588,43 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> /// See [`Query`] for more details. /// /// [System parameter]: crate::system::SystemParam -pub struct Single<'w, D: QueryData, F: QueryFilter = ()> { - pub(crate) item: D::Item<'w>, +/// +/// # Example +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// struct Boss { +/// health: f32 +/// }; +/// +/// fn hurt_boss(mut boss: Single<&mut Boss>) { +/// boss.health -= 4.0; +/// } +/// ``` +/// Note that because [`Single`] implements [`Deref`] and [`DerefMut`], methods and fields like `health` can be accessed directly. +/// You can also access the underlying data manually, by calling `.deref`/`.deref_mut`, or by using the `*` operator. +pub struct Single<'w, 's, D: QueryData, F: QueryFilter = ()> { + pub(crate) item: D::Item<'w, 's>, pub(crate) _filter: PhantomData, } -impl<'w, D: QueryData, F: QueryFilter> Deref for Single<'w, D, F> { - type Target = D::Item<'w>; +impl<'w, 's, D: QueryData, F: QueryFilter> Deref for Single<'w, 's, D, F> { + type Target = D::Item<'w, 's>; fn deref(&self) -> &Self::Target { &self.item } } -impl<'w, D: QueryData, F: QueryFilter> DerefMut for Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> DerefMut for Single<'w, 's, D, F> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.item } } -impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> Single<'w, 's, D, F> { /// Returns the inner item with ownership. - pub fn into_inner(self) -> D::Item<'w> { + pub fn into_inner(self) -> D::Item<'w, 's> { self.item } } @@ -2627,6 +2665,36 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Populated<'w, 's, D, F> { } } +impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Populated<'w, 's, D, F> { + type Item = as IntoIterator>::Item; + + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, 'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'a Populated<'w, 's, D, F> { + type Item = <&'a Query<'w, 's, D, F> as IntoIterator>::Item; + + type IntoIter = <&'a Query<'w, 's, D, F> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.deref().into_iter() + } +} + +impl<'a, 'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'a mut Populated<'w, 's, D, F> { + type Item = <&'a mut Query<'w, 's, D, F> as IntoIterator>::Item; + + type IntoIter = <&'a mut Query<'w, 's, D, F> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.deref_mut().into_iter() + } +} + #[cfg(test)] mod tests { use crate::{prelude::*, query::QueryEntityError}; diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 8ef7d9ed57..ca4bdd4460 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -1,15 +1,15 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, system::{input::SystemIn, BoxedSystem, System, SystemInput}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; -use super::{IntoSystem, SystemParamValidationError}; +use super::{IntoSystem, SystemParamValidationError, SystemStateFlags}; /// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`] pub struct InfallibleSystemWrapper>(S); @@ -26,38 +26,17 @@ impl> System for InfallibleSystemWrapper { type Out = Result; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.0.name() } - #[inline] - fn component_access(&self) -> &Access { - self.0.component_access() + fn type_id(&self) -> core::any::TypeId { + self.0.type_id() } #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.0.component_access_set() - } - - #[inline(always)] - fn archetype_component_access(&self) -> &Access { - self.0.archetype_component_access() - } - - #[inline] - fn is_send(&self) -> bool { - self.0.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.0.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.0.has_deferred() + fn flags(&self) -> SystemStateFlags { + self.0.flags() } #[inline] @@ -70,6 +49,12 @@ impl> System for InfallibleSystemWrapper { Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.0.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.0.apply_deferred(world); @@ -89,18 +74,13 @@ impl> System for InfallibleSystemWrapper { } #[inline] - fn initialize(&mut self, world: &mut World) { - self.0.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.0.initialize(world) } #[inline] - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.0.update_archetype_component_access(world); - } - - #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.0.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.0.check_change_tick(check); } #[inline] @@ -158,35 +138,15 @@ where T: Send + Sync + 'static, { type In = (); - type Out = S::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - - fn archetype_component_access(&self) -> &Access { - self.system.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -197,6 +157,12 @@ where self.system.run_unsafe(&mut self.value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -212,16 +178,12 @@ where self.system.validate_param_unsafe(world) } - fn initialize(&mut self, world: &mut World) { - self.system.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.system.initialize(world) } - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.system.update_archetype_component_access(world); - } - - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { @@ -269,35 +231,15 @@ where T: FromWorld + Send + Sync + 'static, { type In = (); - type Out = S::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - - fn archetype_component_access(&self) -> &Access { - self.system.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -312,6 +254,12 @@ where self.system.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -327,19 +275,15 @@ where self.system.validate_param_unsafe(world) } - fn initialize(&mut self, world: &mut World) { - self.system.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { if self.value.is_none() { self.value = Some(T::from_world(world)); } + self.system.initialize(world) } - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.system.update_archetype_component_access(world); - } - - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 18ec7f44cd..d4521e76f5 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -2,24 +2,37 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17353." )] +use bevy_utils::prelude::DebugName; +use bitflags::bitflags; use core::fmt::Debug; use log::warn; use thiserror::Error; use crate::{ - archetype::ArchetypeComponentId, - component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + component::{CheckChangeTicks, ComponentId, Tick}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; -use alloc::{borrow::Cow, boxed::Box, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; use core::any::TypeId; use super::{IntoSystem, SystemParamValidationError}; +bitflags! { + /// Bitflags representing system states and requirements. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct SystemStateFlags: u8 { + /// Set if system cannot be sent across threads + const NON_SEND = 1 << 0; + /// Set if system requires exclusive World access + const EXCLUSIVE = 1 << 1; + /// Set if system has deferred buffers. + const DEFERRED = 1 << 2; + } +} /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule) /// /// Systems are functions with all arguments implementing @@ -37,30 +50,35 @@ pub trait System: Send + Sync + 'static { type In: SystemInput; /// The system's output. type Out; + /// Returns the system's name. - fn name(&self) -> Cow<'static, str>; + fn name(&self) -> DebugName; /// Returns the [`TypeId`] of the underlying system type. #[inline] fn type_id(&self) -> TypeId { TypeId::of::() } - /// Returns the system's component [`Access`]. - fn component_access(&self) -> &Access; + /// Returns the [`SystemStateFlags`] of the system. + fn flags(&self) -> SystemStateFlags; - /// Returns the system's component [`FilteredAccessSet`]. - fn component_access_set(&self) -> &FilteredAccessSet; - - /// Returns the system's archetype component [`Access`]. - fn archetype_component_access(&self) -> &Access; /// Returns true if the system is [`Send`]. - fn is_send(&self) -> bool; + #[inline] + fn is_send(&self) -> bool { + !self.flags().intersects(SystemStateFlags::NON_SEND) + } /// Returns true if the system must be run exclusively. - fn is_exclusive(&self) -> bool; + #[inline] + fn is_exclusive(&self) -> bool { + self.flags().intersects(SystemStateFlags::EXCLUSIVE) + } /// Returns true if system has deferred buffers. - fn has_deferred(&self) -> bool; + #[inline] + fn has_deferred(&self) -> bool { + self.flags().intersects(SystemStateFlags::DEFERRED) + } /// Runs the system with the given input in the world. Unlike [`System::run`], this function /// can be called in parallel with other systems and may break Rust's aliasing rules @@ -72,16 +90,17 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `archetype_component_access`. There must be no conflicting + /// registered in the access returned from [`System::initialize`]. There must be no conflicting /// simultaneous accesses while the system is running. /// - If [`System::is_exclusive`] returns `true`, then it must be valid to call /// [`UnsafeWorldCell::world_mut`] on `world`. - /// - The method [`System::update_archetype_component_access`] must be called at some - /// point before this one, with the same exact [`World`]. If [`System::update_archetype_component_access`] - /// panics (or otherwise does not return for any reason), this method must not be called. unsafe fn run_unsafe(&mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell) -> Self::Out; + /// Refresh the inner pointer based on the latest hot patch jump table + #[cfg(feature = "hotpatching")] + fn refresh_hotpatch(&mut self); + /// Runs the system with the given input in the world. /// /// For [read-only](ReadOnlySystem) systems, see [`run_readonly`], which can be called using `&World`. @@ -104,10 +123,8 @@ pub trait System: Send + Sync + 'static { world: &mut World, ) -> Self::Out { let world_cell = world.as_unsafe_world_cell(); - self.update_archetype_component_access(world_cell); // SAFETY: // - We have exclusive access to the entire world. - // - `update_archetype_component_access` has been called. unsafe { self.run_unsafe(input, world_cell) } } @@ -123,7 +140,7 @@ pub trait System: Send + Sync + 'static { /// Validates that all parameters can be acquired and that system can run without panic. /// Built-in executors use this to prevent invalid systems from running. /// - /// However calling and respecting [`System::validate_param_unsafe`] or it's safe variant + /// However calling and respecting [`System::validate_param_unsafe`] or its safe variant /// is not a strict requirement, both [`System::run`] and [`System::run_unsafe`] /// should provide their own safety mechanism to prevent undefined behavior. /// @@ -134,11 +151,8 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `archetype_component_access`. There must be no conflicting + /// registered in the access returned from [`System::initialize`]. There must be no conflicting /// simultaneous accesses while the system is running. - /// - The method [`System::update_archetype_component_access`] must be called at some - /// point before this one, with the same exact [`World`]. If [`System::update_archetype_component_access`] - /// panics (or otherwise does not return for any reason), this method must not be called. unsafe fn validate_param_unsafe( &mut self, world: UnsafeWorldCell, @@ -148,28 +162,21 @@ pub trait System: Send + Sync + 'static { /// that runs on exclusive, single-threaded `world` pointer. fn validate_param(&mut self, world: &World) -> Result<(), SystemParamValidationError> { let world_cell = world.as_unsafe_world_cell_readonly(); - self.update_archetype_component_access(world_cell); // SAFETY: // - We have exclusive access to the entire world. - // - `update_archetype_component_access` has been called. unsafe { self.validate_param_unsafe(world_cell) } } /// Initialize the system. - fn initialize(&mut self, _world: &mut World); - - /// Update the system's archetype component [`Access`]. /// - /// ## Note for implementers - /// `world` may only be used to access metadata. This can be done in safe code - /// via functions such as [`UnsafeWorldCell::archetypes`]. - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell); + /// Returns a [`FilteredAccessSet`] with the access required to run the system. + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet; /// Checks any [`Tick`]s stored on this system and wraps their value if they get too old. /// /// This method must be called periodically to ensure that change detection behaves correctly. /// When using bevy's default configuration, this will be called for you as needed. - fn check_change_tick(&mut self, change_tick: Tick); + fn check_change_tick(&mut self, check: CheckChangeTicks); /// Returns the system's default [system sets](crate::schedule::SystemSet). /// @@ -210,10 +217,8 @@ pub unsafe trait ReadOnlySystem: System { /// since this system is known not to modify the world. fn run_readonly(&mut self, input: SystemIn<'_, Self>, world: &World) -> Self::Out { let world = world.as_unsafe_world_cell_readonly(); - self.update_archetype_component_access(world); // SAFETY: // - We have read-only access to the entire world. - // - `update_archetype_component_access` has been called. unsafe { self.run_unsafe(input, world) } } } @@ -221,9 +226,13 @@ pub unsafe trait ReadOnlySystem: System { /// A convenience type alias for a boxed [`System`] trait object. pub type BoxedSystem = Box>; -pub(crate) fn check_system_change_tick(last_run: &mut Tick, this_run: Tick, system_name: &str) { - if last_run.check_tick(this_run) { - let age = this_run.relative_to(*last_run).get(); +pub(crate) fn check_system_change_tick( + last_run: &mut Tick, + check: CheckChangeTicks, + system_name: DebugName, +) { + if last_run.check_tick(check) { + let age = check.present_tick().relative_to(*last_run).get(); warn!( "System '{system_name}' has not run for {age} ticks. \ Changes older than {} ticks will not be detected.", @@ -391,7 +400,7 @@ pub enum RunSystemError { #[error("System {system} did not run due to failed parameter validation: {err}")] InvalidParams { /// The identifier of the system that was run. - system: Cow<'static, str>, + system: DebugName, /// The returned parameter validation error. err: SystemParamValidationError, }, @@ -401,7 +410,6 @@ pub enum RunSystemError { mod tests { use super::*; use crate::prelude::*; - use alloc::string::ToString; #[test] fn run_system_once() { @@ -474,7 +482,5 @@ mod tests { let result = world.run_system_once(system); assert!(matches!(result, Err(RunSystemError::InvalidParams { .. }))); - let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist"; - assert_eq!(expected, result.unwrap_err().to_string()); } } diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index b28ddd89f6..e0c3c952cf 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -1,12 +1,12 @@ use crate::{ - component::Tick, + component::{ComponentId, Tick}, prelude::World, + query::FilteredAccessSet, system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}, world::unsafe_world_cell::UnsafeWorldCell, }; -use alloc::borrow::Cow; -use core::ops::Deref; -use derive_more::derive::{AsRef, Display, Into}; +use bevy_utils::prelude::DebugName; +use derive_more::derive::{Display, Into}; /// [`SystemParam`] that returns the name of the system which it is used in. /// @@ -19,11 +19,11 @@ use derive_more::derive::{AsRef, Display, Into}; /// # use bevy_ecs::system::SystemParam; /// /// #[derive(SystemParam)] -/// struct Logger<'s> { -/// system_name: SystemName<'s>, +/// struct Logger { +/// system_name: SystemName, /// } /// -/// impl<'s> Logger<'s> { +/// impl Logger { /// fn log(&mut self, message: &str) { /// eprintln!("{}: {}", self.system_name, message); /// } @@ -34,61 +34,58 @@ use derive_more::derive::{AsRef, Display, Into}; /// logger.log("Hello"); /// } /// ``` -#[derive(Debug, Into, Display, AsRef)] -#[as_ref(str)] -pub struct SystemName<'s>(&'s str); +#[derive(Debug, Into, Display)] +pub struct SystemName(DebugName); -impl<'s> SystemName<'s> { +impl SystemName { /// Gets the name of the system. - pub fn name(&self) -> &str { - self.0 - } -} - -impl<'s> Deref for SystemName<'s> { - type Target = str; - fn deref(&self) -> &Self::Target { - self.name() + pub fn name(&self) -> DebugName { + self.0.clone() } } // SAFETY: no component value access -unsafe impl SystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'w, 's> = SystemName<'s>; +unsafe impl SystemParam for SystemName { + type State = (); + type Item<'w, 's> = SystemName; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { } #[inline] unsafe fn get_param<'w, 's>( - name: &'s mut Self::State, - _system_meta: &SystemMeta, + _state: &'s mut Self::State, + system_meta: &SystemMeta, _world: UnsafeWorldCell<'w>, _change_tick: Tick, ) -> Self::Item<'w, 's> { - SystemName(name) + SystemName(system_meta.name.clone()) } } // SAFETY: Only reads internal system state -unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} +unsafe impl ReadOnlySystemParam for SystemName {} -impl ExclusiveSystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'s> = SystemName<'s>; +impl ExclusiveSystemParam for SystemName { + type State = (); + type Item<'s> = SystemName; - fn init(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() - } + fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { - SystemName(state) + fn get_param<'s>(_state: &'s mut Self::State, system_meta: &SystemMeta) -> Self::Item<'s> { + SystemName(system_meta.name.clone()) } } #[cfg(test)] +#[cfg(feature = "trace")] mod tests { use crate::{ system::{IntoSystem, RunSystemOnce, SystemName}, @@ -99,7 +96,7 @@ mod tests { #[test] fn test_system_name_regular_param() { fn testing(name: SystemName) -> String { - name.name().to_owned() + name.name().as_string() } let mut world = World::default(); @@ -111,7 +108,7 @@ mod tests { #[test] fn test_system_name_exclusive_param() { fn testing(_world: &mut World, name: SystemName) -> String { - name.name().to_owned() + name.name().as_string() } let mut world = World::default(); @@ -125,7 +122,7 @@ mod tests { let mut world = World::default(); let system = IntoSystem::into_system(|name: SystemName| name.name().to_owned()).with_name("testing"); - let name = world.run_system_once(system).unwrap(); + let name = world.run_system_once(system).unwrap().as_string(); assert_eq!(name, "testing"); } @@ -135,7 +132,7 @@ mod tests { let system = IntoSystem::into_system(|_world: &mut World, name: SystemName| name.name().to_owned()) .with_name("testing"); - let name = world.run_system_once(system).unwrap(); + let name = world.run_system_once(system).unwrap().as_string(); assert_eq!(name, "testing"); } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 6ee7e08fb5..d86d71b9d6 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -1,6 +1,6 @@ pub use crate::change_detection::{NonSendMut, Res, ResMut}; use crate::{ - archetype::{Archetype, Archetypes}, + archetype::Archetypes, bundle::Bundles, change_detection::{MaybeLocation, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Tick}, @@ -23,8 +23,9 @@ use alloc::{ vec::Vec, }; pub use bevy_ecs_macros::SystemParam; +use bevy_platform::cell::SyncCell; use bevy_ptr::UnsafeCellDeref; -use bevy_utils::synccell::SyncCell; +use bevy_utils::prelude::DebugName; use core::{ any::Any, fmt::{Debug, Display}, @@ -32,7 +33,6 @@ use core::{ ops::{Deref, DerefMut}, panic::Location, }; -use disqualified::ShortName; use thiserror::Error; use super::Populated; @@ -57,7 +57,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # use bevy_ecs::prelude::*; /// # #[derive(Resource)] /// # struct SomeResource; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct SomeEvent; /// # #[derive(Resource)] /// # struct SomeOtherResource; @@ -151,7 +151,8 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// let mut world = World::new(); /// let err = world.run_system_cached(|param: MyParam| {}).unwrap_err(); /// let expected = "Parameter `MyParam::foo` failed validation: Custom Message"; -/// assert!(err.to_string().ends_with(expected)); +/// # #[cfg(feature="Trace")] // Without debug_utils/debug enabled MyParam::foo is stripped and breaks the assert +/// assert!(err.to_string().contains(expected)); /// ``` /// /// ## Builders @@ -206,7 +207,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # Safety /// /// The implementor must ensure the following is true. -/// - [`SystemParam::init_state`] correctly registers all [`World`] accesses used +/// - [`SystemParam::init_access`] correctly registers all [`World`] accesses used /// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). /// - None of the world accesses may conflict with any prior accesses registered /// on `system_meta`. @@ -220,25 +221,16 @@ pub unsafe trait SystemParam: Sized { /// You could think of [`SystemParam::Item<'w, 's>`] as being an *operation* that changes the lifetimes bound to `Self`. type Item<'world, 'state>: SystemParam; - /// Registers any [`World`] access used by this [`SystemParam`] - /// and creates a new instance of this param's [`State`](SystemParam::State). - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State; + /// Creates a new instance of this param's [`State`](SystemParam::State). + fn init_state(world: &mut World) -> Self::State; - /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a - /// - /// # Safety - /// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`]. - #[inline] - #[expect( - unused_variables, - reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." - )] - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + /// Registers any [`World`] access used by this [`SystemParam`] + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, - ) { - } + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ); /// Applies any deferred mutations stored in this [`SystemParam`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). @@ -290,15 +282,14 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have read-only access to world data - /// registered in [`init_state`](SystemParam::init_state). + /// registered in [`init_access`](SystemParam::init_access). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). - /// - All `world`'s archetypes have been processed by [`new_archetype`](SystemParam::new_archetype). #[expect( unused_variables, reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." )] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -310,9 +301,8 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have access to any world data registered - /// in [`init_state`](SystemParam::init_state). + /// in [`init_access`](SystemParam::init_access). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). - /// - All `world`'s archetypes have been processed by [`new_archetype`](SystemParam::new_archetype). unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, @@ -336,24 +326,31 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re { } -// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. unsafe impl SystemParam for Query<'_, '_, D, F> { type State = QueryState; type Item<'w, 's> = Query<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let state = QueryState::new_with_access(world, &mut system_meta.archetype_component_access); - init_query_param(world, system_meta, &state); - state + fn init_state(world: &mut World) -> Self::State { + QueryState::new(world) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - state.new_archetype(archetype, &mut system_meta.archetype_component_access); + assert_component_access_compatibility( + &system_meta.name, + DebugName::type_name::(), + DebugName::type_name::(), + component_access_set, + &state.component_access, + world, + ); + component_access_set.add(state.component_access.clone()); } #[inline] @@ -367,32 +364,14 @@ unsafe impl SystemParam for Qu // so the caller ensures that `world` has permission to access any // world data that the query needs. // The caller ensures the world matches the one used in init_state. - unsafe { state.query_unchecked_manual_with_ticks(world, system_meta.last_run, change_tick) } + unsafe { state.query_unchecked_with_ticks(world, system_meta.last_run, change_tick) } } } -pub(crate) fn init_query_param( - world: &mut World, - system_meta: &mut SystemMeta, - state: &QueryState, -) { - assert_component_access_compatibility( - &system_meta.name, - core::any::type_name::(), - core::any::type_name::(), - &system_meta.component_access_set, - &state.component_access, - world, - ); - system_meta - .component_access_set - .add(state.component_access.clone()); -} - fn assert_component_access_compatibility( - system_name: &str, - query_type: &'static str, - filter_type: &'static str, + system_name: &DebugName, + query_type: DebugName, + filter_type: DebugName, system_access: &FilteredAccessSet, current: &FilteredAccess, world: &World, @@ -406,26 +385,28 @@ fn assert_component_access_compatibility( if !accesses.is_empty() { accesses.push(' '); } - panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001", ShortName(query_type), ShortName(filter_type)); + panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001", query_type.shortname(), filter_type.shortname()); } -// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. -unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam for Single<'a, D, F> { +unsafe impl<'a, 'b, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam + for Single<'a, 'b, D, F> +{ type State = QueryState; - type Item<'w, 's> = Single<'w, D, F>; + type Item<'w, 's> = Single<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Query::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + Query::init_state(world) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: Delegate to existing `SystemParam` implementations. - unsafe { Query::new_archetype(state, archetype, system_meta) }; + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -437,9 +418,8 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo ) -> Self::Item<'w, 's> { // SAFETY: State ensures that the components it accesses are not accessible somewhere elsewhere. // The caller ensures the world matches the one used in init_state. - let query = unsafe { - state.query_unchecked_manual_with_ticks(world, system_meta.last_run, change_tick) - }; + let query = + unsafe { state.query_unchecked_with_ticks(world, system_meta.last_run, change_tick) }; let single = query .single_inner() .expect("The query was expected to contain exactly one matching entity."); @@ -451,7 +431,7 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -459,11 +439,7 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo // and the query is read only. // The caller ensures the world matches the one used in init_state. let query = unsafe { - state.query_unchecked_manual_with_ticks( - world, - system_meta.last_run, - world.change_tick(), - ) + state.query_unchecked_with_ticks(world, system_meta.last_run, world.change_tick()) }; match query.single_inner() { Ok(_) => Ok(()), @@ -478,12 +454,12 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo } // SAFETY: QueryState is constrained to read-only fetches, so it only reads World. -unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam - for Single<'a, D, F> +unsafe impl<'a, 'b, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam + for Single<'a, 'b, D, F> { } -// SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If +// SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. unsafe impl SystemParam for Populated<'_, '_, D, F> @@ -491,17 +467,17 @@ unsafe impl SystemParam type State = QueryState; type Item<'w, 's> = Populated<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Query::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + Query::init_state(world) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: Delegate to existing `SystemParam` implementations. - unsafe { Query::new_archetype(state, archetype, system_meta) }; + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -518,7 +494,7 @@ unsafe impl SystemParam #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -526,11 +502,7 @@ unsafe impl SystemParam // - We have read-only access to the components accessed by query. // - The caller ensures the world matches the one used in init_state. let query = unsafe { - state.query_unchecked_manual_with_ticks( - world, - system_meta.last_run, - world.change_tick(), - ) + state.query_unchecked_with_ticks(world, system_meta.last_run, world.change_tick()) }; if query.is_empty() { Err(SystemParamValidationError::skipped::( @@ -629,7 +601,7 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # impl MyEvent { /// # pub fn new() -> Self { Self } @@ -669,13 +641,13 @@ pub struct ParamSet<'w, 's, T: SystemParam> { } macro_rules! impl_param_set { - ($(($index: tt, $param: ident, $system_meta: ident, $fn_name: ident)),*) => { + ($(($index: tt, $param: ident, $fn_name: ident)),*) => { // SAFETY: All parameters are constrained to ReadOnlySystemParam, so World is only read unsafe impl<'w, 's, $($param,)*> ReadOnlySystemParam for ParamSet<'w, 's, ($($param,)*)> where $($param: ReadOnlySystemParam,)* { } - // SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts + // SAFETY: Relevant parameter ComponentId access is applied to SystemMeta. If any ParamState conflicts // with any prior access, a panic will occur. unsafe impl<'_w, '_s, $($param: SystemParam,)*> SystemParam for ParamSet<'_w, '_s, ($($param,)*)> { @@ -690,34 +662,32 @@ macro_rules! impl_param_set { non_snake_case, reason = "Certain variable names are provided by the caller, not by us." )] - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - $( - // Pretend to add each param to the system alone, see if it conflicts - let mut $system_meta = system_meta.clone(); - $system_meta.component_access_set.clear(); - $system_meta.archetype_component_access.clear(); - $param::init_state(world, &mut $system_meta); - // The variable is being defined with non_snake_case here - let $param = $param::init_state(world, &mut system_meta.clone()); - )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false $(|| !$system_meta.is_send())* { - system_meta.set_non_send(); - } - $( - system_meta - .component_access_set - .extend($system_meta.component_access_set); - system_meta - .archetype_component_access - .extend(&$system_meta.archetype_component_access); - )* - ($($param,)*) + fn init_state(world: &mut World) -> Self::State { + ($($param::init_state(world),)*) } - unsafe fn new_archetype(state: &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { <($($param,)*) as SystemParam>::new_archetype(state, archetype, system_meta); } + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro meant for tuples; as such, `non_snake_case` won't always lint." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] + fn init_access(state: &Self::State, system_meta: &mut SystemMeta, component_access_set: &mut FilteredAccessSet, world: &mut World) { + let ($($param,)*) = state; + $( + // Call `init_access` on a clone of the original access set to check for conflicts + let component_access_set_clone = &mut component_access_set.clone(); + $param::init_access($param, system_meta, component_access_set_clone, world); + )* + $( + // Pretend to add the param to the system alone to gather the new access, + // then merge its access into the system. + let mut access_set = FilteredAccessSet::new(); + $param::init_access($param, system_meta, &mut access_set, world); + component_access_set.extend(access_set); + )* } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -730,7 +700,7 @@ macro_rules! impl_param_set { #[inline] unsafe fn validate_param<'w, 's>( - state: &'s Self::State, + state: &'s mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'w>, ) -> Result<(), SystemParamValidationError> { @@ -773,42 +743,41 @@ macro_rules! impl_param_set { } } -all_tuples_enumerated!(impl_param_set, 1, 8, P, m, p); +all_tuples_enumerated!(impl_param_set, 1, 8, P, p); // SAFETY: Res only reads a single World resource unsafe impl<'a, T: Resource> ReadOnlySystemParam for Res<'a, T> {} -// SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res +// SAFETY: Res ComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { type State = ComponentId; type Item<'w, 's> = Res<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let component_id = world.components_registrator().register_resource::(); - let archetype_component_id = world.initialize_resource_internal(component_id).id(); + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_resource::() + } - let combined_access = system_meta.component_access_set.combined_access(); + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), - "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), + "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - system_meta - .archetype_component_access - .add_resource_read(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -840,8 +809,8 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { panic!( "Resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); Res { value: ptr.deref(), @@ -856,40 +825,38 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { } } -// SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res +// SAFETY: Res ComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { type State = ComponentId; type Item<'w, 's> = ResMut<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let component_id = world.components_registrator().register_resource::(); - let archetype_component_id = world.initialize_resource_internal(component_id).id(); + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_resource::() + } - let combined_access = system_meta.component_access_set.combined_access(); + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); if combined_access.has_resource_write(component_id) { panic!( - "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name); } else if combined_access.has_resource_read(component_id) { panic!( - "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - system_meta - .archetype_component_access - .add_resource_write(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -920,8 +887,8 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { panic!( "Resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); ResMut { value: value.value.deref_mut::(), @@ -944,28 +911,24 @@ unsafe impl SystemParam for &'_ World { type State = (); type Item<'w, 's> = &'w World; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let mut access = Access::default(); - access.read_all(); - if !system_meta - .archetype_component_access - .is_compatible(&access) - { - panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules"); - } - system_meta.archetype_component_access.extend(&access); + fn init_state(_world: &mut World) -> Self::State {} + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { let mut filtered_access = FilteredAccess::default(); filtered_access.read_all(); - if !system_meta - .component_access_set + if !component_access_set .get_conflicts_single(&filtered_access) .is_empty() { panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules"); } - system_meta.component_access_set.add(filtered_access); + component_access_set.add(filtered_access); } #[inline] @@ -985,17 +948,20 @@ unsafe impl<'w> SystemParam for DeferredWorld<'w> { type State = (); type Item<'world, 'state> = DeferredWorld<'world>; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { assert!( - !system_meta - .component_access_set - .combined_access() - .has_any_read(), + !component_access_set.combined_access().has_any_read(), "DeferredWorld in system {} conflicts with a previous access.", system_meta.name, ); - system_meta.component_access_set.write_all(); - system_meta.archetype_component_access.write_all(); + component_access_set.write_all(); } unsafe fn get_param<'world, 'state>( @@ -1125,10 +1091,18 @@ unsafe impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { type State = SyncCell; type Item<'w, 's> = Local<'s, T>; - fn init_state(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { SyncCell::new(T::from_world(world)) } + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } + #[inline] unsafe fn get_param<'w, 's>( state: &'s mut Self::State, @@ -1305,11 +1279,19 @@ unsafe impl SystemParam for Deferred<'_, T> { type State = SyncCell; type Item<'w, 's> = Deferred<'s, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.set_has_deferred(); + fn init_state(world: &mut World) -> Self::State { SyncCell::new(T::from_world(world)) } + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + system_meta.set_has_deferred(); + } + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { state.get().apply(system_meta, world); } @@ -1338,7 +1320,14 @@ unsafe impl SystemParam for NonSendMarker { type Item<'w, 's> = Self; #[inline] - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); } @@ -1411,6 +1400,7 @@ impl<'w, T> Deref for NonSend<'w, T> { self.value } } + impl<'a, T> From> for NonSend<'a, T> { fn from(nsm: NonSendMut<'a, T>) -> Self { Self { @@ -1426,39 +1416,37 @@ impl<'a, T> From> for NonSend<'a, T> { } } -// SAFETY: NonSendComponentId and ArchetypeComponentId access is applied to SystemMeta. If this +// SAFETY: NonSendComponentId access is applied to SystemMeta. If this // NonSend conflicts with any prior access, a panic will occur. unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { type State = ComponentId; type Item<'w, 's> = NonSend<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_non_send::() + } + + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); - let component_id = world.components_registrator().register_non_send::(); - let archetype_component_id = world.initialize_non_send_internal(component_id).id(); - - let combined_access = system_meta.component_access_set.combined_access(); + let combined_access = component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), - "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), + "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - - system_meta - .archetype_component_access - .add_resource_read(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1490,7 +1478,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { panic!( "Non-send resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() + DebugName::type_name::() ) }); @@ -1504,42 +1492,40 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { } } -// SAFETY: NonSendMut ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this +// SAFETY: NonSendMut ComponentId access is applied to SystemMeta. If this // NonSendMut conflicts with any prior access, a panic will occur. unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { type State = ComponentId; type Item<'w, 's> = NonSendMut<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_non_send::() + } + + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); - let component_id = world.components_registrator().register_non_send::(); - let archetype_component_id = world.initialize_non_send_internal(component_id).id(); - - let combined_access = system_meta.component_access_set.combined_access(); + let combined_access = component_access_set.combined_access(); if combined_access.has_component_write(component_id) { panic!( - "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name); } else if combined_access.has_component_read(component_id) { panic!( - "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - system_meta - .archetype_component_access - .add_resource_write(archetype_component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] unsafe fn validate_param( - &component_id: &Self::State, + &mut component_id: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1571,8 +1557,8 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { panic!( "Non-send resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); NonSendMut { value: ptr.assert_unique().deref_mut(), @@ -1590,7 +1576,15 @@ unsafe impl<'a> SystemParam for &'a Archetypes { type State = (); type Item<'w, 's> = &'w Archetypes; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1611,7 +1605,15 @@ unsafe impl<'a> SystemParam for &'a Components { type State = (); type Item<'w, 's> = &'w Components; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1632,7 +1634,15 @@ unsafe impl<'a> SystemParam for &'a Entities { type State = (); type Item<'w, 's> = &'w Entities; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1653,7 +1663,15 @@ unsafe impl<'a> SystemParam for &'a Bundles { type State = (); type Item<'w, 's> = &'w Bundles; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1675,7 +1693,7 @@ unsafe impl<'a> SystemParam for &'a Bundles { /// Component change ticks that are more recent than `last_run` will be detected by the system. /// Those can be read by calling [`last_changed`](crate::change_detection::DetectChanges::last_changed) /// on a [`Mut`](crate::change_detection::Mut) or [`ResMut`](ResMut). -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub struct SystemChangeTick { last_run: Tick, this_run: Tick, @@ -1703,7 +1721,15 @@ unsafe impl SystemParam for SystemChangeTick { type State = (); type Item<'w, 's> = SystemChangeTick; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1725,8 +1751,17 @@ unsafe impl SystemParam for Option { type Item<'world, 'state> = Option>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1741,15 +1776,6 @@ unsafe impl SystemParam for Option { .map(|()| T::get_param(state, system_meta, world, change_tick)) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) }; - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { T::apply(state, system_meta, world); } @@ -1768,8 +1794,17 @@ unsafe impl SystemParam for Result = Result, SystemParamValidationError>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1783,15 +1818,6 @@ unsafe impl SystemParam for Result SystemParam for When { type Item<'world, 'state> = When>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1889,15 +1924,6 @@ unsafe impl SystemParam for When { When(T::get_param(state, system_meta, world, change_tick)) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) }; - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { T::apply(state, system_meta, world); } @@ -1910,21 +1936,31 @@ unsafe impl SystemParam for When { // SAFETY: Delegates to `T`, which ensures the safety requirements are met unsafe impl ReadOnlySystemParam for When {} -// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Registers access for each element of `state`. +// If any one conflicts, it will panic. unsafe impl SystemParam for Vec { type State = Vec; type Item<'world, 'state> = Vec>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Vec::new() } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + for state in state { + T::init_access(state, system_meta, component_access_set, world); + } + } + #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -1944,23 +1980,12 @@ unsafe impl SystemParam for Vec { state .iter_mut() // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by each param. + // - We initialized the access for each parameter in `init_access`, so the caller ensures we have access to any world data needed by each param. // - The caller ensures this was the world used to initialize our state, and we used that world to initialize parameter states .map(|state| unsafe { T::get_param(state, system_meta, world, change_tick) }) .collect() } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - for state in state { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) }; - } - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { for state in state { T::apply(state, system_meta, world); @@ -1974,18 +1999,38 @@ unsafe impl SystemParam for Vec { } } -// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Registers access for each element of `state`. +// If any one conflicts with a previous parameter, +// the call passing a copy of the current access will panic. unsafe impl SystemParam for ParamSet<'_, '_, Vec> { type State = Vec; type Item<'world, 'state> = ParamSet<'world, 'state, Vec>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Vec::new() } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + for state in state { + // Call `init_access` on a clone of the original access set to check for conflicts + let component_access_set_clone = &mut component_access_set.clone(); + T::init_access(state, system_meta, component_access_set_clone, world); + } + for state in state { + // Pretend to add the param to the system alone to gather the new access, + // then merge its access into the system. + let mut access_set = FilteredAccessSet::new(); + T::init_access(state, system_meta, &mut access_set, world); + component_access_set.extend(access_set); + } + } + #[inline] unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, @@ -2001,17 +2046,6 @@ unsafe impl SystemParam for ParamSet<'_, '_, Vec> { } } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - for state in state { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(state, archetype, system_meta) } - } - } - fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { for state in state { T::apply(state, system_meta, world); @@ -2030,7 +2064,7 @@ impl ParamSet<'_, '_, Vec> { /// No other parameters may be accessed while this one is active. pub fn get_mut(&mut self, index: usize) -> T::Item<'_, '_> { // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // - We initialized the access for each parameter, so the caller ensures we have access to any world data needed by any param. // We have mutable access to the ParamSet, so no other params in the set are active. // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states unsafe { @@ -2048,7 +2082,7 @@ impl ParamSet<'_, '_, Vec> { self.param_states.iter_mut().for_each(|state| { f( // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // - We initialized the access for each parameter, so the caller ensures we have access to any world data needed by any param. // We have mutable access to the ParamSet, so no other params in the set are active. // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states unsafe { T::get_param(state, &self.system_meta, self.world, self.change_tick) }, @@ -2082,18 +2116,13 @@ macro_rules! impl_system_param_tuple { type Item<'w, 's> = ($($param::Item::<'w, 's>,)*); #[inline] - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - (($($param::init_state(world, system_meta),)*)) + fn init_state(world: &mut World) -> Self::State { + (($($param::init_state(world),)*)) } - #[inline] - unsafe fn new_archetype(($($param,)*): &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { - #[allow( - unused_unsafe, - reason = "Zero-length tuples will not run anything in the unsafe block." - )] - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { $($param::new_archetype($param, archetype, system_meta);)* } + fn init_access(state: &Self::State, _system_meta: &mut SystemMeta, _component_access_set: &mut FilteredAccessSet, _world: &mut World) { + let ($($param,)*) = state; + $($param::init_access($param, _system_meta, _component_access_set, _world);)* } #[inline] @@ -2112,7 +2141,7 @@ macro_rules! impl_system_param_tuple { #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -2261,17 +2290,17 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, type State = P::State; type Item<'world, 'state> = StaticSystemParam<'world, 'state, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - P::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + P::init_state(world) } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: The caller guarantees that the provided `archetype` matches the World used to initialize `state`. - unsafe { P::new_archetype(state, archetype, system_meta) }; + P::init_access(state, system_meta, component_access_set, world); } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2284,7 +2313,7 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -2308,7 +2337,15 @@ unsafe impl SystemParam for PhantomData { type State = (); type Item<'world, 'state> = Self; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'world, 'state>( @@ -2510,16 +2547,7 @@ impl DynSystemParamState { } /// Allows a [`SystemParam::State`] to be used as a trait object for implementing [`DynSystemParam`]. -trait DynParamState: Sync + Send { - /// Casts the underlying `ParamState` to an `Any` so it can be downcast. - fn as_any_mut(&mut self) -> &mut dyn Any; - - /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable).a - /// - /// # Safety - /// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`]. - unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta); - +trait DynParamState: Sync + Send + Any { /// Applies any deferred mutations stored in this [`SystemParam`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). /// @@ -2529,12 +2557,20 @@ trait DynParamState: Sync + Send { /// Queues any deferred mutations to be applied at the next [`ApplyDeferred`](crate::prelude::ApplyDeferred). fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld); + /// Registers any [`World`] access used by this [`SystemParam`] + fn init_access( + &self, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ); + /// Refer to [`SystemParam::validate_param`]. /// /// # Safety /// Refer to [`SystemParam::validate_param`]. unsafe fn validate_param( - &self, + &mut self, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError>; @@ -2544,15 +2580,6 @@ trait DynParamState: Sync + Send { struct ParamState(T::State); impl DynParamState for ParamState { - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - unsafe fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { T::new_archetype(&mut self.0, archetype, system_meta) }; - } - fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) { T::apply(&mut self.0, system_meta, world); } @@ -2561,28 +2588,48 @@ impl DynParamState for ParamState { T::queue(&mut self.0, system_meta, world); } - unsafe fn validate_param( + fn init_access( &self, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(&self.0, system_meta, component_access_set, world); + } + + unsafe fn validate_param( + &mut self, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - T::validate_param(&self.0, system_meta, world) + T::validate_param(&mut self.0, system_meta, world) } } -// SAFETY: `init_state` creates a state of (), which performs no access. The interesting safety checks are on the `SystemParamBuilder`. +// SAFETY: Delegates to the wrapped parameter, which ensures the safety requirements are met unsafe impl SystemParam for DynSystemParam<'_, '_> { type State = DynSystemParamState; type Item<'world, 'state> = DynSystemParam<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { DynSystemParamState::new::<()>(()) } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + state + .0 + .init_access(system_meta, component_access_set, world); + } + #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -2597,27 +2644,11 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { change_tick: Tick, ) -> Self::Item<'world, 'state> { // SAFETY: - // - `state.0` is a boxed `ParamState`, and its implementation of `as_any_mut` returns `self`. - // - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used - // by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). + // - `state.0` is a boxed `ParamState`. + // - `init_access` calls `DynParamState::init_access`, which calls `init_access` on the inner parameter, + // so the caller ensures the world has the necessary access. // - The caller ensures that the provided world is the same and has the required access. - unsafe { - DynSystemParam::new( - state.0.as_any_mut(), - world, - system_meta.clone(), - change_tick, - ) - } - } - - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &Archetype, - system_meta: &mut SystemMeta, - ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { state.0.new_archetype(archetype, system_meta) }; + unsafe { DynSystemParam::new(state.0.as_mut(), world, system_meta.clone(), change_tick) } } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2629,26 +2660,48 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { } } -// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResources` with no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Resource ComponentId access is applied to the access. If this FilteredResources +// conflicts with any prior access, a panic will occur. unsafe impl SystemParam for FilteredResources<'_, '_> { type State = Access; type Item<'world, 'state> = FilteredResources<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Access::new() } + fn init_access( + access: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &system_meta.name; + panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); + } + + if access.has_read_all_resources() { + component_access_set.add_unfiltered_read_all_resources(); + } else { + for component_id in access.resource_reads_and_writes() { + component_access_set.add_unfiltered_resource_read(component_id); + } + } + } + unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, - // and the builder registers `access` in `build`. + // SAFETY: The caller ensures that `world` has access to anything registered in `init_access`, + // and we registered all resource access in `state``. unsafe { FilteredResources::new(world, state, system_meta.last_run, change_tick) } } } @@ -2656,26 +2709,56 @@ unsafe impl SystemParam for FilteredResources<'_, '_> { // SAFETY: FilteredResources only reads resources. unsafe impl ReadOnlySystemParam for FilteredResources<'_, '_> {} -// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResourcesMut` with no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Resource ComponentId access is applied to the access. If this FilteredResourcesMut +// conflicts with any prior access, a panic will occur. unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { type State = Access; type Item<'world, 'state> = FilteredResourcesMut<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Access::new() } + fn init_access( + access: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &system_meta.name; + panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); + } + + if access.has_read_all_resources() { + component_access_set.add_unfiltered_read_all_resources(); + } else { + for component_id in access.resource_reads() { + component_access_set.add_unfiltered_resource_read(component_id); + } + } + + if access.has_write_all_resources() { + component_access_set.add_unfiltered_write_all_resources(); + } else { + for component_id in access.resource_writes() { + component_access_set.add_unfiltered_resource_write(component_id); + } + } + } + unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, - // and the builder registers `access` in `build`. + // SAFETY: The caller ensures that `world` has access to anything registered in `init_access`, + // and we registered all resource access in `state``. unsafe { FilteredResourcesMut::new(world, state, system_meta.last_run, change_tick) } } } @@ -2706,7 +2789,7 @@ pub struct SystemParamValidationError { /// A string identifying the invalid parameter. /// This is usually the type name of the parameter. - pub param: Cow<'static, str>, + pub param: DebugName, /// A string identifying the field within a parameter using `#[derive(SystemParam)]`. /// This will be an empty string for other parameters. @@ -2738,7 +2821,7 @@ impl SystemParamValidationError { Self { skipped, message: message.into(), - param: Cow::Borrowed(core::any::type_name::()), + param: DebugName::type_name::(), field: field.into(), } } @@ -2749,17 +2832,21 @@ impl Display for SystemParamValidationError { write!( fmt, "Parameter `{}{}` failed validation: {}", - ShortName(&self.param), + self.param.shortname(), self.field, self.message - ) + )?; + if !self.skipped { + write!(fmt, "\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens.")?; + } + Ok(()) } } #[cfg(test)] mod tests { use super::*; - use crate::system::assert_is_system; + use crate::{event::Event, system::assert_is_system}; use core::cell::RefCell; // Compile test for https://github.com/bevyengine/bevy/pull/2838. @@ -2991,7 +3078,7 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_resource_error::res_system`: Parameter `Res` failed validation: Resource does not exist"] + #[should_panic] fn missing_resource_error() { #[derive(Resource)] pub struct MissingResource; @@ -3005,11 +3092,11 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: Event not initialized"] + #[should_panic] fn missing_event_error() { - use crate::prelude::{Event, EventReader}; + use crate::prelude::{BufferedEvent, EventReader}; - #[derive(Event)] + #[derive(Event, BufferedEvent)] pub struct MissingEvent; let mut schedule = crate::schedule::Schedule::default(); diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index cf53b35be5..e9c9cdba13 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -651,6 +651,19 @@ mod tests { assert_eq!(output, NonCopy(3)); } + #[test] + fn fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn exclusive_system() { let mut world = World::new(); @@ -751,19 +764,54 @@ mod tests { assert!(matches!(output, Ok(x) if x == four())); } + #[test] + fn cached_fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system_cached(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached(sys); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached_with(sys, ()); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn cached_system_commands() { fn sys(mut counter: ResMut) { - counter.0 = 1; + counter.0 += 1; } let mut world = World::new(); world.insert_resource(Counter(0)); - world.commands().run_system_cached(sys); world.flush_commands(); - assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); + } + + #[test] + fn cached_fallible_system_commands() { + fn sys(mut counter: ResMut) -> Result { + counter.0 += 1; + Ok(()) + } + + let mut world = World::new(); + world.insert_resource(Counter(0)); + world.commands().run_system_cached(sys); + world.flush_commands(); + assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); } #[test] @@ -865,7 +913,6 @@ mod tests { #[test] fn run_system_invalid_params() { use crate::system::RegisteredSystemError; - use alloc::{format, string::ToString}; struct T; impl Resource for T {} @@ -880,8 +927,6 @@ mod tests { result, Err(RegisteredSystemError::InvalidParams { .. }) )); - let expected = format!("System {id:?} did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist"); - assert_eq!(expected, result.unwrap_err().to_string()); } #[test] diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 342ad47849..577720fd5d 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -1,6 +1,10 @@ //! A trait for components that let you traverse the ECS. -use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship}; +use crate::{ + entity::Entity, + query::{ReadOnlyQueryData, ReleaseStateQueryData}, + relationship::Relationship, +}; /// A component that can point to another entity, and which can be used to define a path through the ECS. /// @@ -10,23 +14,23 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// 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 cause your application to hang. Each implementer of `Traversal` -/// for documenting possible looping behavior, and consumers of those implementations are responsible for +/// is responsible for documenting possible looping behavior, and consumers of those implementations are responsible for /// avoiding infinite loops in their code. /// /// Traversals may be parameterized with additional data. For example, in observer event propagation, the -/// parameter `D` is the event type given in `Trigger`. This allows traversal to differ depending on event +/// parameter `D` is the event type given in `On`. This allows traversal to differ depending on event /// data. /// -/// [specify the direction]: crate::event::Event::Traversal -/// [event propagation]: crate::observer::Trigger::propagate +/// [specify the direction]: crate::event::EntityEvent::Traversal +/// [event propagation]: crate::observer::On::propagate /// [observers]: crate::observer::Observer -pub trait Traversal: ReadOnlyQueryData { +pub trait Traversal: ReadOnlyQueryData + ReleaseStateQueryData { /// Returns the next entity to visit. - fn traverse(item: Self::Item<'_>, data: &D) -> Option; + fn traverse(item: Self::Item<'_, '_>, data: &D) -> Option; } impl Traversal for () { - fn traverse(_: Self::Item<'_>, _data: &D) -> Option { + fn traverse(_: Self::Item<'_, '_>, _data: &D) -> Option { None } } @@ -37,9 +41,9 @@ impl Traversal for () { /// /// Traversing in a loop could result in infinite loops for relationship graphs with loops. /// -/// [event propagation]: crate::observer::Trigger::propagate +/// [event propagation]: crate::observer::On::propagate impl Traversal for &R { - fn traverse(item: Self::Item<'_>, _data: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _data: &D) -> Option { Some(item.get()) } } diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index e8f820c066..243de1955c 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -423,12 +423,12 @@ mod test { let mut world = World::new(); queue.apply(&mut world); - assert_eq!(world.entities().len(), 2); + assert_eq!(world.entity_count(), 2); // The previous call to `apply` cleared the queue. // This call should do nothing. queue.apply(&mut world); - assert_eq!(world.entities().len(), 2); + assert_eq!(world.entity_count(), 2); } #[expect( @@ -462,7 +462,7 @@ mod test { queue.push(SpawnCommand); queue.push(SpawnCommand); queue.apply(&mut world); - assert_eq!(world.entities().len(), 3); + assert_eq!(world.entity_count(), 3); } #[test] diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs deleted file mode 100644 index ea2899c5f9..0000000000 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Internal components used by bevy with a fixed component id. -//! Constants are used to skip [`TypeId`] lookups in hot paths. -use super::*; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -/// [`ComponentId`] for [`OnAdd`] -pub const ON_ADD: ComponentId = ComponentId::new(0); -/// [`ComponentId`] for [`OnInsert`] -pub const ON_INSERT: ComponentId = ComponentId::new(1); -/// [`ComponentId`] for [`OnReplace`] -pub const ON_REPLACE: ComponentId = ComponentId::new(2); -/// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// [`ComponentId`] for [`OnDespawn`] -pub const ON_DESPAWN: ComponentId = ComponentId::new(4); - -/// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. -/// See [`crate::component::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; - -/// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. -/// See [`crate::component::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; - -/// Trigger emitted when a component is inserted onto an entity that already has that component. -/// Runs before the value is replaced, so you can still access the original component data. -/// See [`crate::component::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; - -/// Trigger emitted when a component is removed from an entity, and runs before the component is -/// removed, so you can still access the component data. -/// See [`crate::component::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; - -/// Trigger emitted for each component on an entity when it is despawned. -/// See [`crate::component::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 02c12fe6a3..1699eadcff 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -1,11 +1,14 @@ use core::ops::Deref; +use bevy_utils::prelude::DebugName; + use crate::{ archetype::Archetype, change_detection::{MaybeLocation, MutUntyped}, - component::{ComponentId, HookContext, Mutable}, + component::{ComponentId, Mutable}, entity::Entity, - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EntityEvent, Event, EventId, Events, SendBatchIds}, + lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, @@ -16,14 +19,14 @@ use crate::{ world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; -use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. /// /// This means that in order to add entities, for example, you will need to use commands instead of the world directly. pub struct DeferredWorld<'w> { - // SAFETY: Implementors must not use this reference to make structural changes + // SAFETY: Implementers must not use this reference to make structural changes world: UnsafeWorldCell<'w>, } @@ -84,7 +87,7 @@ impl<'w> DeferredWorld<'w> { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -93,9 +96,11 @@ impl<'w> DeferredWorld<'w> { /// If you do not need to ensure the above hooks are triggered, and your component /// is mutable, prefer using [`get_mut`](DeferredWorld::get_mut). #[inline] - pub(crate) fn modify_component( + #[track_caller] + pub(crate) fn modify_component_with_relationship_hook_mode( &mut self, entity: Entity, + relationship_hook_mode: RelationshipHookMode, f: impl FnOnce(&mut T) -> R, ) -> Result, EntityMutableFetchError> { // If the component is not registered, then it doesn't exist on this entity, so no action required. @@ -103,18 +108,23 @@ impl<'w> DeferredWorld<'w> { return Ok(None); }; - self.modify_component_by_id(entity, component_id, move |component| { - // SAFETY: component matches the component_id collected in the above line - let mut component = unsafe { component.with_type::() }; + self.modify_component_by_id_with_relationship_hook_mode( + entity, + component_id, + relationship_hook_mode, + move |component| { + // SAFETY: component matches the component_id collected in the above line + let mut component = unsafe { component.with_type::() }; - f(&mut component) - }) + f(&mut component) + }, + ) } /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -123,13 +133,15 @@ impl<'w> DeferredWorld<'w> { /// If you do not need to ensure the above hooks are triggered, and your component /// is mutable, prefer using [`get_mut_by_id`](DeferredWorld::get_mut_by_id). /// - /// You should prefer the typed [`modify_component`](DeferredWorld::modify_component) + /// You should prefer the typed [`modify_component_with_relationship_hook_mode`](DeferredWorld::modify_component_with_relationship_hook_mode) /// whenever possible. #[inline] - pub(crate) fn modify_component_by_id( + #[track_caller] + pub(crate) fn modify_component_by_id_with_relationship_hook_mode( &mut self, entity: Entity, component_id: ComponentId, + relationship_hook_mode: RelationshipHookMode, f: impl for<'a> FnOnce(MutUntyped<'a>) -> R, ) -> Result, EntityMutableFetchError> { let entity_cell = self.get_entity_mut(entity)?; @@ -144,7 +156,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_replace( @@ -152,12 +164,12 @@ impl<'w> DeferredWorld<'w> { entity, [component_id].into_iter(), MaybeLocation::caller(), - RelationshipHookMode::Run, + relationship_hook_mode, ); if archetype.has_replace_observer() { self.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -184,7 +196,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_insert( @@ -192,12 +204,12 @@ impl<'w> DeferredWorld<'w> { entity, [component_id].into_iter(), MaybeLocation::caller(), - RelationshipHookMode::Run, + relationship_hook_mode, ); if archetype.has_insert_observer() { self.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -450,7 +462,7 @@ impl<'w> DeferredWorld<'w> { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -479,7 +491,7 @@ impl<'w> DeferredWorld<'w> { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -495,34 +507,34 @@ impl<'w> DeferredWorld<'w> { unsafe { self.world.get_non_send_resource_mut() } } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { let Some(mut events_resource) = self.get_resource_mut::>() else { log::error!( "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - core::any::type_name::() + DebugName::type_name::() ); return None; }; @@ -738,7 +750,7 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers( &mut self, event: ComponentId, - target: Entity, + target: Option, components: impl Iterator + Clone, caller: MaybeLocation, ) { @@ -746,6 +758,7 @@ impl<'w> DeferredWorld<'w> { self.reborrow(), event, target, + target, components, &mut (), &mut false, @@ -761,7 +774,8 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - mut target: Entity, + current_target: Option, + original_target: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -769,41 +783,64 @@ impl<'w> DeferredWorld<'w> { ) where T: Traversal, { + Observers::invoke::<_>( + self.reborrow(), + event, + current_target, + original_target, + components.clone(), + data, + &mut propagate, + caller, + ); + let Some(mut current_target) = current_target else { + return; + }; + loop { + if !propagate { + return; + } + if let Some(traverse_to) = self + .get_entity(current_target) + .ok() + .and_then(|entity| entity.get_components::()) + .and_then(|item| T::traverse(item, data)) + { + current_target = traverse_to; + } else { + break; + } Observers::invoke::<_>( self.reborrow(), event, - target, + Some(current_target), + original_target, components.clone(), data, &mut propagate, caller, ); - if !propagate { - break; - } - if let Some(traverse_to) = self - .get_entity(target) - .ok() - .and_then(|entity| entity.get_components::()) - .and_then(|item| T::traverse(item, data)) - { - target = traverse_to; - } else { - break; - } } } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [`Event`] without any targets. + /// + /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger(&mut self, trigger: impl Event) { self.commands().trigger(trigger); } - /// Sends a [`Trigger`](crate::observer::Trigger) with the given `targets`. + /// Sends an [`EntityEvent`] with the given `targets` + /// + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger_targets( &mut self, - trigger: impl Event, + trigger: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.commands().trigger_targets(trigger, targets); diff --git a/crates/bevy_ecs/src/world/entity_fetch.rs b/crates/bevy_ecs/src/world/entity_fetch.rs index 8588131563..4aa8baf9e8 100644 --- a/crates/bevy_ecs/src/world/entity_fetch.rs +++ b/crates/bevy_ecs/src/world/entity_fetch.rs @@ -200,6 +200,7 @@ unsafe impl WorldEntityFetch for Entity { type Mut<'w> = EntityWorldMut<'w>; type DeferredMut<'w> = EntityMut<'w>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -209,6 +210,7 @@ unsafe impl WorldEntityFetch for Entity { Ok(unsafe { EntityRef::new(ecell) }) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -220,9 +222,10 @@ unsafe impl WorldEntityFetch for Entity { // SAFETY: caller ensures that the world cell has mutable access to the entity. let world = unsafe { cell.world_mut() }; // SAFETY: location was fetched from the same world's `Entities`. - Ok(unsafe { EntityWorldMut::new(world, self, location) }) + Ok(unsafe { EntityWorldMut::new(world, self, Some(location)) }) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -242,6 +245,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { type Mut<'w> = [EntityMut<'w>; N]; type DeferredMut<'w> = [EntityMut<'w>; N]; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -249,6 +253,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { <&Self>::fetch_ref(&self, cell) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -256,6 +261,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { <&Self>::fetch_mut(&self, cell) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -273,6 +279,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { type Mut<'w> = [EntityMut<'w>; N]; type DeferredMut<'w> = [EntityMut<'w>; N]; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -290,6 +297,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -316,6 +324,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -335,6 +344,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { type Mut<'w> = Vec>; type DeferredMut<'w> = Vec>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -349,6 +359,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -372,6 +383,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -391,6 +403,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { type Mut<'w> = EntityHashMap>; type DeferredMut<'w> = EntityHashMap>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -404,6 +417,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -417,6 +431,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 64610f8e4e..d29c3db428 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,5 +1,5 @@ use crate::{ - archetype::{Archetype, ArchetypeId}, + archetype::Archetype, bundle::{ Bundle, BundleEffect, BundleFromComponents, BundleInserter, BundleRemover, DynamicBundle, InsertMode, @@ -10,18 +10,17 @@ use crate::{ StorageType, Tick, }, entity::{ - ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityLocation, + ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, + EntityIdLocation, EntityLocation, }, - event::Event, + event::EntityEvent, + lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, - query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, - world::{ - error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World, - ON_DESPAWN, ON_REMOVE, ON_REPLACE, - }, + world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World}, }; use alloc::vec::Vec; use bevy_platform::collections::{HashMap, HashSet}; @@ -280,14 +279,16 @@ impl<'w> EntityRef<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'w> { + pub fn components(&self) -> Q::Item<'w, 'static> { self.get_components::() .expect("Query does not match the current entity") } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { // SAFETY: We have read-only access to all components of this entity. unsafe { self.cell.get_components::() } } @@ -456,6 +457,7 @@ impl<'w> EntityMut<'w> { /// - `cell` must have permission to mutate every component of the entity. /// - No accesses to any of the entity's components may exist /// at the same time as the returned [`EntityMut`]. + #[inline] pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { Self { cell } } @@ -546,13 +548,15 @@ impl<'w> EntityMut<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1009,6 +1013,7 @@ impl<'w> From> for EntityMut<'w> { } impl<'a> From<&'a mut EntityWorldMut<'_>> for EntityMut<'a> { + #[inline] fn from(entity: &'a mut EntityWorldMut<'_>) -> Self { // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. unsafe { EntityMut::new(entity.as_unsafe_entity_cell()) } @@ -1096,7 +1101,7 @@ unsafe impl EntityEquivalent for EntityMut<'_> {} pub struct EntityWorldMut<'w> { world: &'w mut World, entity: Entity, - location: EntityLocation, + location: EntityIdLocation, } impl<'w> EntityWorldMut<'w> { @@ -1116,43 +1121,48 @@ impl<'w> EntityWorldMut<'w> { #[inline(always)] #[track_caller] pub(crate) fn assert_not_despawned(&self) { - if self.location.archetype_id == ArchetypeId::INVALID { - self.panic_despawned(); + if self.location.is_none() { + self.panic_despawned() } } + #[inline(always)] fn as_unsafe_entity_cell_readonly(&self) -> UnsafeEntityCell<'_> { - self.assert_not_despawned(); + let location = self.location(); let last_change_tick = self.world.last_change_tick; let change_tick = self.world.read_change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell_readonly(), self.entity, - self.location, + location, last_change_tick, change_tick, ) } + + #[inline(always)] fn as_unsafe_entity_cell(&mut self) -> UnsafeEntityCell<'_> { - self.assert_not_despawned(); + let location = self.location(); let last_change_tick = self.world.last_change_tick; let change_tick = self.world.change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, - self.location, + location, last_change_tick, change_tick, ) } + + #[inline(always)] fn into_unsafe_entity_cell(self) -> UnsafeEntityCell<'w> { - self.assert_not_despawned(); + let location = self.location(); let last_change_tick = self.world.last_change_tick; let change_tick = self.world.change_tick(); UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, - self.location, + location, last_change_tick, change_tick, ) @@ -1168,10 +1178,10 @@ impl<'w> EntityWorldMut<'w> { pub(crate) unsafe fn new( world: &'w mut World, entity: Entity, - location: EntityLocation, + location: Option, ) -> Self { debug_assert!(world.entities().contains(entity)); - debug_assert_eq!(world.entities().get(entity), Some(location)); + debug_assert_eq!(world.entities().get(entity), location); EntityWorldMut { world, @@ -1187,6 +1197,7 @@ impl<'w> EntityWorldMut<'w> { } /// Gets read-only access to all of the entity's components. + #[inline] pub fn as_readonly(&self) -> EntityRef<'_> { EntityRef::from(self) } @@ -1198,6 +1209,7 @@ impl<'w> EntityWorldMut<'w> { } /// Gets non-structural mutable access to all of the entity's components. + #[inline] pub fn as_mutable(&mut self) -> EntityMut<'_> { EntityMut::from(self) } @@ -1216,8 +1228,10 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn location(&self) -> EntityLocation { - self.assert_not_despawned(); - self.location + match self.location { + Some(loc) => loc, + None => self.panic_despawned(), + } } /// Returns the archetype that the current entity belongs to. @@ -1227,8 +1241,8 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn archetype(&self) -> &Archetype { - self.assert_not_despawned(); - &self.world.archetypes[self.location.archetype_id] + let location = self.location(); + &self.world.archetypes[location.archetype_id] } /// Returns `true` if the current entity has a component of type `T`. @@ -1300,7 +1314,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity does not have the components required by the query `Q` or if the entity /// has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } @@ -1311,7 +1325,9 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1367,7 +1383,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1420,7 +1436,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1830,22 +1846,22 @@ impl<'w> EntityWorldMut<'w> { caller: MaybeLocation, relationship_hook_mode: RelationshipHookMode, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let change_tick = self.world.change_tick(); let mut bundle_inserter = - BundleInserter::new::(self.world, self.location.archetype_id, change_tick); + BundleInserter::new::(self.world, location.archetype_id, change_tick); // SAFETY: location matches current entity. `T` matches `bundle_info` let (location, after_effect) = unsafe { bundle_inserter.insert( self.entity, - self.location, + location, bundle, mode, caller, relationship_hook_mode, ) }; - self.location = location; + self.location = Some(location); self.world.flush(); self.update_location(); after_effect.apply(self); @@ -1894,7 +1910,7 @@ impl<'w> EntityWorldMut<'w> { caller: MaybeLocation, relationship_hook_insert_mode: RelationshipHookMode, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let change_tick = self.world.change_tick(); let bundle_id = self.world.bundles.init_component_info( &mut self.world.storages, @@ -1903,23 +1919,19 @@ impl<'w> EntityWorldMut<'w> { ); let storage_type = self.world.bundles.get_storage_unchecked(bundle_id); - let bundle_inserter = BundleInserter::new_with_id( - self.world, - self.location.archetype_id, - bundle_id, - change_tick, - ); + let bundle_inserter = + BundleInserter::new_with_id(self.world, location.archetype_id, bundle_id, change_tick); - self.location = insert_dynamic_bundle( + self.location = Some(insert_dynamic_bundle( bundle_inserter, self.entity, - self.location, + location, Some(component).into_iter(), Some(storage_type).iter().cloned(), mode, caller, relationship_hook_insert_mode, - ); + )); self.world.flush(); self.update_location(); self @@ -1957,7 +1969,7 @@ impl<'w> EntityWorldMut<'w> { iter_components: I, relationship_hook_insert_mode: RelationshipHookMode, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let change_tick = self.world.change_tick(); let bundle_id = self.world.bundles.init_dynamic_info( &mut self.world.storages, @@ -1966,23 +1978,19 @@ impl<'w> EntityWorldMut<'w> { ); let mut storage_types = core::mem::take(self.world.bundles.get_storages_unchecked(bundle_id)); - let bundle_inserter = BundleInserter::new_with_id( - self.world, - self.location.archetype_id, - bundle_id, - change_tick, - ); + let bundle_inserter = + BundleInserter::new_with_id(self.world, location.archetype_id, bundle_id, change_tick); - self.location = insert_dynamic_bundle( + self.location = Some(insert_dynamic_bundle( bundle_inserter, self.entity, - self.location, + location, iter_components, (*storage_types).iter().cloned(), InsertMode::Replace, MaybeLocation::caller(), relationship_hook_insert_mode, - ); + )); *self.world.bundles.get_storages_unchecked(bundle_id) = core::mem::take(&mut storage_types); self.world.flush(); self.update_location(); @@ -2000,13 +2008,12 @@ impl<'w> EntityWorldMut<'w> { #[must_use] #[track_caller] pub fn take(&mut self) -> Option { - self.assert_not_despawned(); + let location = self.location(); let entity = self.entity; - let location = self.location; let mut remover = // SAFETY: The archetype id must be valid since this entity is in it. - unsafe { BundleRemover::new::(self.world, self.location.archetype_id, true) }?; + unsafe { BundleRemover::new::(self.world, location.archetype_id, true) }?; // SAFETY: The passed location has the sane archetype as the remover, since they came from the same location. let (new_location, result) = unsafe { remover.remove( @@ -2041,7 +2048,7 @@ impl<'w> EntityWorldMut<'w> { }, ) }; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); @@ -2062,11 +2069,11 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn remove_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let Some(mut remover) = // SAFETY: The archetype id must be valid since this entity is in it. - (unsafe { BundleRemover::new::(self.world, self.location.archetype_id, false) }) + (unsafe { BundleRemover::new::(self.world, location.archetype_id, false) }) else { return self; }; @@ -2074,14 +2081,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2101,7 +2108,7 @@ impl<'w> EntityWorldMut<'w> { &mut self, caller: MaybeLocation, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let storages = &mut self.world.storages; let bundles = &mut self.world.bundles; // SAFETY: These come from the same world. @@ -2112,7 +2119,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2120,14 +2127,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2147,7 +2154,7 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn retain_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { - self.assert_not_despawned(); + let old_location = self.location(); let archetypes = &mut self.world.archetypes; let storages = &mut self.world.storages; // SAFETY: These come from the same world. @@ -2161,7 +2168,6 @@ impl<'w> EntityWorldMut<'w> { .register_info::(&mut registrator, storages); // SAFETY: `retained_bundle` exists as we just initialized it. let retained_bundle_info = unsafe { self.world.bundles.get_unchecked(retained_bundle) }; - let old_location = self.location; let old_archetype = &mut archetypes[old_location.archetype_id]; // PERF: this could be stored in an Archetype Edge @@ -2176,7 +2182,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, remove_bundle, false) + BundleRemover::new_with_id(self.world, old_location.archetype_id, remove_bundle, false) }) else { return self; }; @@ -2184,14 +2190,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + old_location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2216,7 +2222,7 @@ impl<'w> EntityWorldMut<'w> { component_id: ComponentId, caller: MaybeLocation, ) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let components = &mut self.world.components; let bundle_id = self.world.bundles.init_component_info( @@ -2227,7 +2233,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2235,14 +2241,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2258,7 +2264,7 @@ impl<'w> EntityWorldMut<'w> { /// entity has been despawned while this `EntityWorldMut` is still alive. #[track_caller] pub fn remove_by_ids(&mut self, component_ids: &[ComponentId]) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let components = &mut self.world.components; let bundle_id = self.world.bundles.init_dynamic_info( @@ -2269,7 +2275,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2277,14 +2283,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, MaybeLocation::caller(), BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2302,7 +2308,7 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub(crate) fn clear_with_caller(&mut self, caller: MaybeLocation) -> &mut Self { - self.assert_not_despawned(); + let location = self.location(); let component_ids: Vec = self.archetype().components().collect(); let components = &mut self.world.components; @@ -2314,7 +2320,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { - BundleRemover::new_with_id(self.world, self.location.archetype_id, bundle_id, false) + BundleRemover::new_with_id(self.world, location.archetype_id, bundle_id, false) }) else { return self; }; @@ -2322,14 +2328,14 @@ impl<'w> EntityWorldMut<'w> { let new_location = unsafe { remover.remove( self.entity, - self.location, + location, caller, BundleRemover::empty_pre_remove, ) } .0; - self.location = new_location; + self.location = Some(new_location); self.world.flush(); self.update_location(); self @@ -2353,9 +2359,9 @@ impl<'w> EntityWorldMut<'w> { } pub(crate) fn despawn_with_caller(self, caller: MaybeLocation) { - self.assert_not_despawned(); + let location = self.location(); let world = self.world; - let archetype = &world.archetypes[self.location.archetype_id]; + let archetype = &world.archetypes[location.archetype_id]; // SAFETY: Archetype cannot be mutably aliased by DeferredWorld let (archetype, mut deferred_world) = unsafe { @@ -2368,8 +2374,8 @@ impl<'w> EntityWorldMut<'w> { unsafe { if archetype.has_despawn_observer() { deferred_world.trigger_observers( - ON_DESPAWN, - self.entity, + DESPAWN, + Some(self.entity), archetype.components(), caller, ); @@ -2382,8 +2388,8 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - self.entity, + REPLACE, + Some(self.entity), archetype.components(), caller, ); @@ -2397,8 +2403,8 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, - self.entity, + REMOVE, + Some(self.entity), archetype.components(), caller, ); @@ -2422,30 +2428,32 @@ impl<'w> EntityWorldMut<'w> { let location = world .entities .free(self.entity) + .flatten() .expect("entity should exist at this point."); let table_row; let moved_entity; let change_tick = world.change_tick(); { - let archetype = &mut world.archetypes[self.location.archetype_id]; + let archetype = &mut world.archetypes[location.archetype_id]; let remove_result = archetype.swap_remove(location.archetype_row); if let Some(swapped_entity) = remove_result.swapped_entity { let swapped_location = world.entities.get(swapped_entity).unwrap(); // SAFETY: swapped_entity is valid and the swapped entity's components are // moved to the new location immediately after. unsafe { - world.entities.set_spawn_despawn( + world.entities.set( swapped_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: swapped_location.archetype_id, archetype_row: location.archetype_row, table_id: swapped_location.table_id, table_row: swapped_location.table_row, - }, - caller, - change_tick, + }), ); + world + .entities + .mark_spawn_despawn(swapped_entity.index(), caller, change_tick); } } table_row = remove_result.table_row; @@ -2466,17 +2474,18 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: `moved_entity` is valid and the provided `EntityLocation` accurately reflects // the current location of the entity and its component data. unsafe { - world.entities.set_spawn_despawn( + world.entities.set( moved_entity.index(), - EntityLocation { + Some(EntityLocation { archetype_id: moved_location.archetype_id, archetype_row: moved_location.archetype_row, table_id: moved_location.table_id, table_row, - }, - caller, - change_tick, + }), ); + world + .entities + .mark_spawn_despawn(moved_entity.index(), caller, change_tick); } world.archetypes[moved_location.archetype_id] .set_entity_table_row(moved_location.archetype_row, table_row); @@ -2564,11 +2573,7 @@ impl<'w> EntityWorldMut<'w> { /// This is *only* required when using the unsafe function [`EntityWorldMut::world_mut`], /// which enables the location to change. pub fn update_location(&mut self) { - self.location = self - .world - .entities() - .get(self.entity) - .unwrap_or(EntityLocation::INVALID); + self.location = self.world.entities().get(self.entity); } /// Returns if the entity has been despawned. @@ -2580,7 +2585,7 @@ impl<'w> EntityWorldMut<'w> { /// to avoid panicking when calling further methods. #[inline] pub fn is_despawned(&self) -> bool { - self.location.archetype_id == ArchetypeId::INVALID + self.location.is_none() } /// Gets an Entry into the world for this entity and component for in-place manipulation. @@ -2608,14 +2613,14 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn entry<'a, T: Component>(&'a mut self) -> Entry<'w, 'a, T> { + pub fn entry<'a, T: Component>(&'a mut self) -> ComponentEntry<'w, 'a, T> { if self.contains::() { - Entry::Occupied(OccupiedEntry { + ComponentEntry::Occupied(OccupiedComponentEntry { entity_world: self, _marker: PhantomData, }) } else { - Entry::Vacant(VacantEntry { + ComponentEntry::Vacant(VacantComponentEntry { entity_world: self, _marker: PhantomData, }) @@ -2627,7 +2632,7 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.assert_not_despawned(); self.world.trigger_targets(event, self.entity); self.world.flush(); @@ -2644,14 +2649,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller( + pub(crate) fn observe_with_caller( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, @@ -2858,14 +2863,14 @@ impl<'w> EntityWorldMut<'w> { /// This `enum` can only be constructed from the [`entry`] method on [`EntityWorldMut`]. /// /// [`entry`]: EntityWorldMut::entry -pub enum Entry<'w, 'a, T: Component> { +pub enum ComponentEntry<'w, 'a, T: Component> { /// An occupied entry. - Occupied(OccupiedEntry<'w, 'a, T>), + Occupied(OccupiedComponentEntry<'w, 'a, T>), /// A vacant entry. - Vacant(VacantEntry<'w, 'a, T>), + Vacant(VacantComponentEntry<'w, 'a, T>), } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { /// Provides in-place mutable access to an occupied entry. /// /// # Examples @@ -2884,17 +2889,17 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { #[inline] pub fn and_modify)>(self, f: F) -> Self { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { f(entry.get_mut()); - Entry::Occupied(entry) + ComponentEntry::Occupied(entry) } - Entry::Vacant(entry) => Entry::Vacant(entry), + ComponentEntry::Vacant(entry) => ComponentEntry::Vacant(entry), } } } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { - /// Replaces the component of the entry, and returns an [`OccupiedEntry`]. +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { + /// Replaces the component of the entry, and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// @@ -2913,13 +2918,13 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(entry.get(), &Comp(2)); /// ``` #[inline] - pub fn insert_entry(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert_entry(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { entry.insert(component); entry } - Entry::Vacant(entry) => entry.insert(component), + ComponentEntry::Vacant(entry) => entry.insert(component), } } @@ -2945,10 +2950,10 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 8); /// ``` #[inline] - pub fn or_insert(self, default: T) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert(self, default: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default), } } @@ -2969,15 +2974,15 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 4); /// ``` #[inline] - pub fn or_insert_with T>(self, default: F) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert_with T>(self, default: F) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default()), } } } -impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component + Default> ComponentEntry<'w, 'a, T> { /// Ensures the entry has this component by inserting the default value if empty, and /// returns a mutable reference to this component in the entry. /// @@ -2995,42 +3000,42 @@ impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 0); /// ``` #[inline] - pub fn or_default(self) -> OccupiedEntry<'w, 'a, T> { + pub fn or_default(self) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(Default::default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(Default::default()), } } } -/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. +/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`OccupiedComponentEntry`] enum. /// /// The contained entity must have the component type parameter if we have this struct. -pub struct OccupiedEntry<'w, 'a, T: Component> { +pub struct OccupiedComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a reference to the component in the entry. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.get().0, 5); /// } /// ``` #[inline] pub fn get(&self) -> &T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get::().unwrap() } @@ -3039,14 +3044,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.insert(Comp(10)); /// } /// @@ -3062,14 +3067,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.take(), Comp(5)); /// } /// @@ -3077,30 +3082,30 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn take(self) -> T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.take().unwrap() } } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a mutable reference to the component in the entry. /// - /// If you need a reference to the `OccupiedEntry` which may outlive the destruction of - /// the `Entry` value, see [`into_mut`]. + /// If you need a reference to the [`OccupiedComponentEntry`] which may outlive the destruction of + /// the [`OccupiedComponentEntry`] value, see [`into_mut`]. /// /// [`into_mut`]: Self::into_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.get_mut().0 += 10; /// assert_eq!(o.get().0, 15); /// @@ -3112,28 +3117,28 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn get_mut(&mut self) -> Mut<'_, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut::().unwrap() } - /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry with + /// Converts the [`OccupiedComponentEntry`] into a mutable reference to the value in the entry with /// a lifetime bound to the `EntityWorldMut`. /// - /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// If you need multiple references to the [`OccupiedComponentEntry`], see [`get_mut`]. /// /// [`get_mut`]: Self::get_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// o.into_mut().0 += 10; /// } /// @@ -3141,40 +3146,40 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn into_mut(self) -> Mut<'a, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut().unwrap() } } -/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. -pub struct VacantEntry<'w, 'a, T: Component> { +/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`ComponentEntry`] enum. +pub struct VacantComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { - /// Inserts the component into the `VacantEntry` and returns an `OccupiedEntry`. +impl<'w, 'a, T: Component> VacantComponentEntry<'w, 'a, T> { + /// Inserts the component into the [`VacantComponentEntry`] and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn_empty(); /// - /// if let Entry::Vacant(v) = entity.entry::() { + /// if let ComponentEntry::Vacant(v) = entity.entry::() { /// v.insert(Comp(10)); /// } /// /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 10); /// ``` #[inline] - pub fn insert(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { self.entity_world.insert(component); - OccupiedEntry { + OccupiedComponentEntry { entity_world: self.entity_world, _marker: PhantomData, } @@ -3185,7 +3190,7 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { /// /// To define the access when used as a [`QueryData`](crate::query::QueryData), /// use a [`QueryBuilder`](crate::query::QueryBuilder) or [`QueryParamBuilder`](crate::system::QueryParamBuilder). -/// The `FilteredEntityRef` must be the entire `QueryData`, and not nested inside a tuple with other data. +/// The [`FilteredEntityRef`] must be the entire [`QueryData`](crate::query::QueryData), and not nested inside a tuple with other data. /// /// ``` /// # use bevy_ecs::{prelude::*, world::FilteredEntityRef}; @@ -3287,7 +3292,11 @@ impl<'w> FilteredEntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get(&self) -> Option<&'w T> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3301,7 +3310,11 @@ impl<'w> FilteredEntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_ref(&self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3313,7 +3326,11 @@ impl<'w> FilteredEntityRef<'w> { /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3645,7 +3662,11 @@ impl<'w> FilteredEntityMut<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_mut>(&mut self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_write(id) // SAFETY: We have write access @@ -3673,7 +3694,11 @@ impl<'w> FilteredEntityMut<'w> { /// - `T` must be a mutable component #[inline] pub unsafe fn into_mut_assume_mutable(self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_write(id) // SAFETY: @@ -3903,7 +3928,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -3923,7 +3948,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -4003,7 +4028,11 @@ where /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - let component_id = self.entity.world().components().get_id(TypeId::of::())?; + let component_id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; let components = self.entity.world().components(); (!bundle_contains_component::(components, component_id)) .then(|| { @@ -4172,7 +4201,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -4731,7 +4760,8 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::{HookContext, Tick}; + use crate::component::Tick; + use crate::lifecycle::HookContext; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -4755,7 +4785,7 @@ mod tests { let entity = world.spawn(TestComponent(42)).id(); let component_id = world .components() - .get_id(core::any::TypeId::of::()) + .get_valid_id(core::any::TypeId::of::()) .unwrap(); let entity = world.entity(entity); @@ -4772,7 +4802,7 @@ mod tests { let entity = world.spawn(TestComponent(42)).id(); let component_id = world .components() - .get_id(core::any::TypeId::of::()) + .get_valid_id(core::any::TypeId::of::()) .unwrap(); let mut entity_mut = world.entity_mut(entity); @@ -5715,7 +5745,7 @@ mod tests { assert_eq!((&mut X(8), &mut Y(9)), (x_component, y_component)); } - #[derive(Event)] + #[derive(Event, EntityEvent)] struct TestEvent; #[test] @@ -5723,7 +5753,7 @@ mod tests { let mut world = World::new(); let entity = world .spawn_empty() - .observe(|trigger: Trigger, mut commands: Commands| { + .observe(|trigger: On, mut commands: Commands| { commands.entity(trigger.target()).insert(TestComponent(0)); }) .id(); @@ -5733,7 +5763,7 @@ mod tests { let mut a = world.entity_mut(entity); a.trigger(TestEvent); // this adds command to change entity archetype - a.observe(|_: Trigger| {}); // this flushes commands implicitly by spawning + a.observe(|_: On| {}); // this flushes commands implicitly by spawning let location = a.location(); assert_eq!(world.entities().get(entity), Some(location)); } @@ -5742,11 +5772,9 @@ mod tests { #[should_panic] fn location_on_despawned_entity_panics() { let mut world = World::new(); - world.add_observer( - |trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target()).despawn(); - }, - ); + world.add_observer(|trigger: On, mut commands: Commands| { + commands.entity(trigger.target()).despawn(); + }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); a.insert(TestComponent(0)); @@ -5764,14 +5792,12 @@ mod tests { fn archetype_modifications_trigger_flush() { let mut world = World::new(); world.insert_resource(TestFlush(0)); - world.add_observer(|_: Trigger, mut commands: Commands| { + world.add_observer(|_: On, mut commands: Commands| { + commands.queue(count_flush); + }); + world.add_observer(|_: On, mut commands: Commands| { commands.queue(count_flush); }); - world.add_observer( - |_: Trigger, mut commands: Commands| { - commands.queue(count_flush); - }, - ); world.commands().queue(count_flush); let entity = world.spawn_empty().id(); assert_eq!(world.resource::().0, 1); @@ -5836,19 +5862,19 @@ mod tests { .push("OrdA hook on_remove"); } - fn ord_a_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_add"); } - fn ord_a_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_insert"); } - fn ord_a_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_replace"); } - fn ord_a_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_remove"); } @@ -5887,19 +5913,19 @@ mod tests { .push("OrdB hook on_remove"); } - fn ord_b_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_add"); } - fn ord_b_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_insert"); } - fn ord_b_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_replace"); } - fn ord_b_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_remove"); } @@ -6175,10 +6201,10 @@ mod tests { world.insert_resource(Tracker { a: false, b: false }); let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.a = true; }); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.b = true; }); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 3527967942..03574331f2 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -1,6 +1,7 @@ //! Contains error types returned by bevy's schedule. use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use crate::{ component::ComponentId, @@ -24,7 +25,7 @@ pub struct TryRunScheduleError(pub InternedScheduleLabel); #[error("Could not insert bundles of type {bundle_type} into the entities with the following IDs because they do not exist: {entities:?}")] pub struct TryInsertBatchError { /// The bundles' type name. - pub bundle_type: &'static str, + pub bundle_type: DebugName, /// The IDs of the provided entities that do not exist. pub entities: Vec, } diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index a9fac308fa..ed3672bef9 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -157,7 +157,7 @@ impl<'w, 's> FilteredResources<'w, 's> { let component_id = self .world .components() - .resource_id::() + .valid_resource_id::() .ok_or(ResourceFetchError::NotRegistered)?; if !self.access.has_resource_read(component_id) { return Err(ResourceFetchError::NoResourceAccess(component_id)); @@ -474,7 +474,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { let component_id = self .world .components() - .resource_id::() + .valid_resource_id::() .ok_or(ResourceFetchError::NotRegistered)?; // SAFETY: THe caller ensures that there are no conflicting borrows. unsafe { self.get_mut_by_id_unchecked(component_id) } diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index 6b1c803e75..51f9a0ee2c 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -1,5 +1,6 @@ use crate::{ - component::Tick, + component::{ComponentId, Tick}, + query::FilteredAccessSet, storage::SparseSetIndex, system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}, world::{FromWorld, World}, @@ -53,7 +54,15 @@ unsafe impl SystemParam for WorldId { type Item<'world, 'state> = WorldId; - fn init_state(_: &mut World, _: &mut SystemMeta) -> Self::State {} + fn init_state(_: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'world, 'state>( diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 87340551af..714c5e1eae 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,7 +1,6 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; -mod component_constants; mod deferred_world; mod entity_fetch; mod entity_ref; @@ -18,37 +17,44 @@ pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, }; +use crate::{ + error::{DefaultErrorHandler, ErrorHandler}, + event::BufferedEvent, + lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, + prelude::{Add, Despawn, Insert, Remove, Replace}, +}; pub use bevy_ecs_macros::FromWorld; -pub use component_constants::*; +use bevy_utils::prelude::DebugName; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ - DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, - Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, TryFromFilteredError, VacantEntry, + ComponentEntry, DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + EntityWorldMut, FilteredEntityMut, FilteredEntityRef, OccupiedComponentEntry, + TryFromFilteredError, VacantComponentEntry, }; pub use filtered_resource::*; pub use identifier::WorldId; pub use spawn_batch::*; use crate::{ - archetype::{ArchetypeId, ArchetypeRow, Archetypes}, + archetype::{ArchetypeId, Archetypes}, bundle::{ Bundle, BundleEffect, BundleInfo, BundleInserter, BundleSpawner, Bundles, InsertMode, NoBundleEffect, }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo, + CheckChangeTicks, Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, - entity::{Entities, Entity, EntityDoesNotExistError, EntityLocation}, + entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, event::{Event, EventId, Events, SendBatchIds}, + lifecycle::RemovedComponentEvents, observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, - removal_detection::RemovedComponentEvents, resource::Resource, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, @@ -146,20 +152,20 @@ impl World { #[inline] fn bootstrap(&mut self) { // The order that we register these events is vital to ensure that the constants are correct! - let on_add = OnAdd::register_component_id(self); - assert_eq!(ON_ADD, on_add); + let on_add = Add::register_component_id(self); + assert_eq!(ADD, on_add); - let on_insert = OnInsert::register_component_id(self); - assert_eq!(ON_INSERT, on_insert); + let on_insert = Insert::register_component_id(self); + assert_eq!(INSERT, on_insert); - let on_replace = OnReplace::register_component_id(self); - assert_eq!(ON_REPLACE, on_replace); + let on_replace = Replace::register_component_id(self); + assert_eq!(REPLACE, on_replace); - let on_remove = OnRemove::register_component_id(self); - assert_eq!(ON_REMOVE, on_remove); + let on_remove = Remove::register_component_id(self); + assert_eq!(REMOVE, on_remove); - let on_despawn = OnDespawn::register_component_id(self); - assert_eq!(ON_DESPAWN, on_despawn); + let on_despawn = Despawn::register_component_id(self); + assert_eq!(DESPAWN, on_despawn); // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); @@ -210,6 +216,14 @@ impl World { &mut self.entities } + /// Retrieves the number of [`Entities`] in the world. + /// + /// This is helpful as a diagnostic, but it can also be used effectively in tests. + #[inline] + pub fn entity_count(&self) -> u32 { + self.entities.len() + } + /// Retrieves this world's [`Archetypes`] collection. #[inline] pub fn archetypes(&self) -> &Archetypes { @@ -256,6 +270,12 @@ impl World { &self.removed_components } + /// Retrieves this world's [`Observers`] list + #[inline] + pub fn observers(&self) -> &Observers { + &self.observers + } + /// Creates a new [`Commands`] instance that writes to the world's command queue /// Use [`World::flush`] to apply all queued commands #[inline] @@ -298,7 +318,7 @@ impl World { &mut self, id: ComponentId, ) -> Option<&mut ComponentHooks> { - assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(id)), "Components hooks cannot be modified if the component already exists in an archetype, use register_component if the component with id {:?} may already be in use", id); + assert!(!self.archetypes.archetypes.iter().any(|a| a.contains(id)), "Components hooks cannot be modified if the component already exists in an archetype, use register_component if the component with id {id:?} may already be in use"); self.components.get_hooks_mut(id) } @@ -530,7 +550,7 @@ impl World { /// Retrieves the [required components](RequiredComponents) for the given component type, if it exists. pub fn get_required_components(&self) -> Option<&RequiredComponents> { - let id = self.components().component_id::()?; + let id = self.components().valid_component_id::()?; let component_info = self.components().get_info(id)?; Some(component_info.required_components()) } @@ -959,18 +979,8 @@ impl World { pub fn iter_entities(&self) -> impl Iterator> + '_ { self.archetypes.iter().flat_map(|archetype| { archetype - .entities() - .iter() - .enumerate() - .map(|(archetype_row, archetype_entity)| { - let entity = archetype_entity.id(); - let location = EntityLocation { - archetype_id: archetype.id(), - archetype_row: ArchetypeRow::new(archetype_row), - table_id: archetype.table_id(), - table_row: archetype_entity.table_row(), - }; - + .entities_with_location() + .map(|(entity, location)| { // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. let cell = UnsafeEntityCell::new( self.as_unsafe_world_cell_readonly(), @@ -992,18 +1002,8 @@ impl World { let world_cell = self.as_unsafe_world_cell(); world_cell.archetypes().iter().flat_map(move |archetype| { archetype - .entities() - .iter() - .enumerate() - .map(move |(archetype_row, archetype_entity)| { - let entity = archetype_entity.id(); - let location = EntityLocation { - archetype_id: archetype.id(), - archetype_row: ArchetypeRow::new(archetype_row), - table_id: archetype.table_id(), - table_row: archetype_entity.table_row(), - }; - + .entities_with_location() + .map(move |(entity, location)| { // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. let cell = UnsafeEntityCell::new( world_cell, @@ -1173,16 +1173,15 @@ impl World { let entity = self.entities.alloc(); let mut bundle_spawner = BundleSpawner::new::(self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent - let (mut entity_location, after_effect) = + let (entity_location, after_effect) = unsafe { bundle_spawner.spawn_non_existent(entity, bundle, caller) }; + let mut entity_location = Some(entity_location); + // SAFETY: command_queue is not referenced anywhere else if !unsafe { self.command_queue.is_empty() } { self.flush(); - entity_location = self - .entities() - .get(entity) - .unwrap_or(EntityLocation::INVALID); + entity_location = self.entities().get(entity); } // SAFETY: entity and location are valid, as they were just created above @@ -1205,10 +1204,11 @@ impl World { // empty let location = unsafe { archetype.allocate(entity, table_row) }; let change_tick = self.change_tick(); + self.entities.set(entity.index(), Some(location)); self.entities - .set_spawn_despawn(entity.index(), location, caller, change_tick); + .mark_spawn_despawn(entity.index(), caller, change_tick); - EntityWorldMut::new(self, entity, location) + EntityWorldMut::new(self, entity, Some(location)) } /// Spawns a batch of entities with the same component [`Bundle`] type. Takes a given @@ -1289,7 +1289,7 @@ impl World { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1319,6 +1319,7 @@ impl World { /// # assert_eq!(world.get::(entity), Some(&Foo(true))); /// ``` #[inline] + #[track_caller] pub fn modify_component( &mut self, entity: Entity, @@ -1326,7 +1327,11 @@ impl World { ) -> Result, EntityMutableFetchError> { let mut world = DeferredWorld::from(&mut *self); - let result = world.modify_component(entity, f)?; + let result = world.modify_component_with_relationship_hook_mode( + entity, + RelationshipHookMode::Run, + f, + )?; self.flush(); Ok(result) @@ -1335,7 +1340,7 @@ impl World { /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1347,6 +1352,7 @@ impl World { /// You should prefer the typed [`modify_component`](World::modify_component) /// whenever possible. #[inline] + #[track_caller] pub fn modify_component_by_id( &mut self, entity: Entity, @@ -1355,7 +1361,12 @@ impl World { ) -> Result, EntityMutableFetchError> { let mut world = DeferredWorld::from(&mut *self); - let result = world.modify_component_by_id(entity, component_id, f)?; + let result = world.modify_component_by_id_with_relationship_hook_mode( + entity, + component_id, + RelationshipHookMode::Run, + f, + )?; self.flush(); Ok(result) @@ -1463,7 +1474,7 @@ impl World { /// assert!(!transform.is_changed()); /// ``` /// - /// [`RemovedComponents`]: crate::removal_detection::RemovedComponents + /// [`RemovedComponents`]: crate::lifecycle::RemovedComponents pub fn clear_trackers(&mut self) { self.removed_components.update(); self.last_change_tick = self.increment_change_tick(); @@ -1642,7 +1653,7 @@ impl World { /// since the last call to [`World::clear_trackers`]. pub fn removed(&self) -> impl Iterator + '_ { self.components - .get_id(TypeId::of::()) + .get_valid_id(TypeId::of::()) .map(|component_id| self.removed_with_id(component_id)) .into_iter() .flatten() @@ -1791,7 +1802,7 @@ impl World { /// Removes the resource of a given type and returns it, if it exists. Otherwise returns `None`. #[inline] pub fn remove_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?; // SAFETY: `component_id` was gotten via looking up the `R` type unsafe { Some(ptr.read::()) } @@ -1810,7 +1821,7 @@ impl World { /// thread than where the value was inserted from. #[inline] pub fn remove_non_send_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, _, _) = self .storages .non_send_resources @@ -1824,7 +1835,7 @@ impl World { #[inline] pub fn contains_resource(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.storages.resources.get(component_id)) .is_some_and(ResourceData::is_present) } @@ -1842,7 +1853,7 @@ impl World { #[inline] pub fn contains_non_send(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.storages.non_send_resources.get(component_id)) .is_some_and(ResourceData::is_present) } @@ -1865,7 +1876,7 @@ impl World { /// was called. pub fn is_resource_added(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .is_some_and(|component_id| self.is_resource_added_by_id(component_id)) } @@ -1896,7 +1907,7 @@ impl World { /// was called. pub fn is_resource_changed(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .is_some_and(|component_id| self.is_resource_changed_by_id(component_id)) } @@ -1921,7 +1932,7 @@ impl World { /// Retrieves the change ticks for the given resource. pub fn get_resource_change_ticks(&self) -> Option { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.get_resource_change_ticks_by_id(component_id)) } @@ -1957,7 +1968,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -1981,7 +1992,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2005,7 +2016,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2169,7 +2180,7 @@ impl World { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2191,7 +2202,7 @@ impl World { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2358,11 +2369,11 @@ impl World { ) }; } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), self.entities.entity_does_not_exist_error_details(entity)); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", DebugName::type_name::(), self.entities.entity_does_not_exist_error_details(entity)); } } } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {first_entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), self.entities.entity_does_not_exist_error_details(first_entity)); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {first_entity}, which {}. See: https://bevy.org/learn/errors/b0003", DebugName::type_name::(), self.entities.entity_does_not_exist_error_details(first_entity)); } } } @@ -2526,7 +2537,7 @@ impl World { Ok(()) } else { Err(TryInsertBatchError { - bundle_type: core::any::type_name::(), + bundle_type: DebugName::type_name::(), entities: invalid_entities, }) } @@ -2560,7 +2571,7 @@ impl World { #[track_caller] pub fn resource_scope(&mut self, f: impl FnOnce(&mut World, Mut) -> U) -> U { self.try_resource_scope(f) - .unwrap_or_else(|| panic!("resource does not exist: {}", core::any::type_name::())) + .unwrap_or_else(|| panic!("resource does not exist: {}", DebugName::type_name::())) } /// Temporarily removes the requested resource from this [`World`] if it exists, runs custom user code, @@ -2577,7 +2588,7 @@ impl World { let last_change_tick = self.last_change_tick(); let change_tick = self.change_tick(); - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, mut ticks, mut caller) = self .storages .resources @@ -2600,7 +2611,7 @@ impl World { assert!(!self.contains_resource::(), "Resource `{}` was inserted during a call to World::resource_scope.\n\ This is not allowed as the original resource is reinserted to the world after the closure is invoked.", - core::any::type_name::()); + DebugName::type_name::()); OwningPtr::make(value, |ptr| { // SAFETY: pointer is of type R @@ -2614,34 +2625,34 @@ impl World { Some(result) } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { let Some(mut events_resource) = self.get_resource_mut::>() else { log::error!( "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - core::any::type_name::() + DebugName::type_name::() ); return None; }; @@ -2709,12 +2720,9 @@ impl World { component_id: ComponentId, ) -> &mut ResourceData { self.flush_components(); - let archetypes = &mut self.archetypes; self.storages .resources - .initialize_with(component_id, &self.components, || { - archetypes.new_archetype_component_id() - }) + .initialize_with(component_id, &self.components) } /// # Panics @@ -2725,28 +2733,32 @@ impl World { component_id: ComponentId, ) -> &mut ResourceData { self.flush_components(); - let archetypes = &mut self.archetypes; self.storages .non_send_resources - .initialize_with(component_id, &self.components, || { - archetypes.new_archetype_component_id() - }) + .initialize_with(component_id, &self.components) } /// Empties queued entities and adds them to the empty [`Archetype`](crate::archetype::Archetype). /// This should be called before doing operations that might operate on queued entities, /// such as inserting a [`Component`]. + #[track_caller] pub(crate) fn flush_entities(&mut self) { + let by = MaybeLocation::caller(); + let at = self.change_tick(); let empty_archetype = self.archetypes.empty_mut(); let table = &mut self.storages.tables[empty_archetype.table_id()]; // PERF: consider pre-allocating space for flushed entities // SAFETY: entity is set to a valid location unsafe { - self.entities.flush(|entity, location| { - // SAFETY: no components are allocated by archetype.allocate() because the archetype - // is empty - *location = empty_archetype.allocate(entity, table.allocate(entity)); - }); + self.entities.flush( + |entity, location| { + // SAFETY: no components are allocated by archetype.allocate() because the archetype + // is empty + *location = Some(empty_archetype.allocate(entity, table.allocate(entity))); + }, + by, + at, + ); } } @@ -2781,6 +2793,7 @@ impl World { /// /// Queued entities will be spawned, and then commands will be applied. #[inline] + #[track_caller] pub fn flush(&mut self) { self.flush_entities(); self.flush_components(); @@ -2951,17 +2964,21 @@ impl World { } /// Iterates all component change ticks and clamps any older than [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). - /// This prevents overflow and thus prevents false positives. + /// This also triggers [`CheckChangeTicks`] observers and returns the same event here. /// - /// **Note:** Does nothing if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] + /// Calling this method prevents [`Tick`]s overflowing and thus prevents false positives when comparing them. + /// + /// **Note:** Does nothing and returns `None` if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] /// times since the previous pass. // TODO: benchmark and optimize - pub fn check_change_ticks(&mut self) { + pub fn check_change_ticks(&mut self) -> Option { let change_tick = self.change_tick(); if change_tick.relative_to(self.last_check_tick).get() < CHECK_TICK_THRESHOLD { - return; + return None; } + let check = CheckChangeTicks(change_tick); + let Storages { ref mut tables, ref mut sparse_sets, @@ -2971,17 +2988,22 @@ impl World { #[cfg(feature = "trace")] let _span = tracing::info_span!("check component ticks").entered(); - tables.check_change_ticks(change_tick); - sparse_sets.check_change_ticks(change_tick); - resources.check_change_ticks(change_tick); - non_send_resources.check_change_ticks(change_tick); - self.entities.check_change_ticks(change_tick); + tables.check_change_ticks(check); + sparse_sets.check_change_ticks(check); + resources.check_change_ticks(check); + non_send_resources.check_change_ticks(check); + self.entities.check_change_ticks(check); if let Some(mut schedules) = self.get_resource_mut::() { - schedules.check_change_ticks(change_tick); + schedules.check_change_ticks(check); } + self.trigger(check); + self.flush(); + self.last_check_tick = change_tick; + + Some(check) } /// Runs both [`clear_entities`](Self::clear_entities) and [`clear_resources`](Self::clear_resources), @@ -3046,6 +3068,16 @@ impl World { // SAFETY: We just initialized the bundle so its id should definitely be valid. unsafe { self.bundles.get(id).debug_checked_unwrap() } } + + /// Convenience method for accessing the world's default error handler, + /// which can be overwritten with [`DefaultErrorHandler`]. + #[inline] + pub fn default_error_handler(&self) -> ErrorHandler { + self.get_resource::() + .copied() + .unwrap_or_default() + .0 + } } impl World { @@ -3391,11 +3423,18 @@ impl World { // Schedule-related methods impl World { - /// Adds the specified [`Schedule`] to the world. The schedule can later be run + /// Adds the specified [`Schedule`] to the world. + /// If a schedule already exists with the same [label](Schedule::label), it will be replaced. + /// + /// The schedule can later be run /// by calling [`.run_schedule(label)`](Self::run_schedule) or by directly /// accessing the [`Schedules`] resource. /// /// The `Schedules` resource will be initialized if it does not already exist. + /// + /// An alternative to this is to call [`Schedules::add_systems()`] with some + /// [`ScheduleLabel`] and let the schedule for that label be created if it + /// does not already exist. pub fn add_schedule(&mut self, schedule: Schedule) { let mut schedules = self.get_resource_or_init::(); schedules.insert(schedule); @@ -3501,6 +3540,7 @@ impl World { /// and system state is cached. /// /// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead. + /// This avoids the need to create a unique [`ScheduleLabel`]. /// /// # Panics /// @@ -3613,6 +3653,7 @@ mod tests { }; use bevy_ecs_macros::Component; use bevy_platform::collections::{HashMap, HashSet}; + use bevy_utils::prelude::DebugName; use core::{ any::TypeId, panic, @@ -3749,7 +3790,7 @@ mod tests { world.insert_resource(TestResource(42)); let component_id = world .components() - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .unwrap(); let resource = world.get_resource_by_id(component_id).unwrap(); @@ -3765,7 +3806,7 @@ mod tests { world.insert_resource(TestResource(42)); let component_id = world .components() - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .unwrap(); { @@ -3796,12 +3837,12 @@ mod tests { let mut iter = world.iter_resources(); let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` assert_eq!(unsafe { ptr.deref::().0 }, 42); let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); assert_eq!( // SAFETY: We know that the resource is of type `TestResource2` unsafe { &ptr.deref::().0 }, @@ -3824,14 +3865,14 @@ mod tests { let mut iter = world.iter_resources_mut(); let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` unsafe { mut_untyped.as_mut().deref_mut::().0 = 43; }; let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource2` unsafe { mut_untyped.as_mut().deref_mut::().0 = "Hello, world?".to_string(); diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index fdd8b28142..aada63bf61 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -4,8 +4,8 @@ use core::any::TypeId; use thiserror::Error; -use alloc::string::{String, ToString}; use bevy_reflect::{Reflect, ReflectFromPtr}; +use bevy_utils::prelude::DebugName; use crate::{prelude::*, world::ComponentId}; @@ -70,17 +70,14 @@ impl World { entity: Entity, type_id: TypeId, ) -> Result<&dyn Reflect, GetComponentReflectError> { - let Some(component_id) = self.components().get_id(type_id) else { + let Some(component_id) = self.components().get_valid_id(type_id) else { return Err(GetComponentReflectError::NoCorrespondingComponentId( type_id, )); }; let Some(comp_ptr) = self.get_by_id(entity, component_id) else { - let component_name = self - .components() - .get_name(component_id) - .map(|name| name.to_string()); + let component_name = self.components().get_name(component_id); return Err(GetComponentReflectError::EntityDoesNotHaveComponent { entity, @@ -158,7 +155,7 @@ impl World { )); }; - let Some(component_id) = self.components().get_id(type_id) else { + let Some(component_id) = self.components().get_valid_id(type_id) else { return Err(GetComponentReflectError::NoCorrespondingComponentId( type_id, )); @@ -166,10 +163,7 @@ impl World { // HACK: Only required for the `None`-case/`else`-branch, but it borrows `self`, which will // already be mutably borrowed by `self.get_mut_by_id()`, and I didn't find a way around it. - let component_name = self - .components() - .get_name(component_id) - .map(|name| name.to_string()); + let component_name = self.components().get_name(component_id).clone(); let Some(comp_mut_untyped) = self.get_mut_by_id(entity, component_id) else { return Err(GetComponentReflectError::EntityDoesNotHaveComponent { @@ -223,7 +217,7 @@ pub enum GetComponentReflectError { component_id: ComponentId, /// The name corresponding to the [`Component`] with the given [`TypeId`], or `None` /// if not available. - component_name: Option, + component_name: Option, }, /// The [`World`] was missing the [`AppTypeRegistry`] resource. diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index ea5f21c22e..38d4333843 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -7,10 +7,11 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation}, + error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, - query::{DebugCheckedUnwrap, ReadOnlyQueryData}, - removal_detection::RemovedComponentEvents, + query::{DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -35,7 +36,7 @@ use thiserror::Error; /// /// This alone is not enough to implement bevy systems where multiple systems can access *disjoint* parts of the world concurrently. For this, bevy stores all values of /// resources and components (and [`ComponentTicks`]) in [`UnsafeCell`]s, and carefully validates disjoint access patterns using -/// APIs like [`System::component_access`](crate::system::System::component_access). +/// APIs like [`System::initialize`](crate::system::System::initialize). /// /// A system then can be executed using [`System::run_unsafe`](crate::system::System::run_unsafe) with a `&World` and use methods with interior mutability to access resource values. /// @@ -198,13 +199,13 @@ impl<'w> UnsafeWorldCell<'w> { /// /// # Safety /// - must have permission to access the whole world immutably - /// - there must be no live exclusive borrows on world data + /// - there must be no live exclusive borrows of world data /// - there must be no live exclusive borrow of world #[inline] pub unsafe fn world(self) -> &'w World { // SAFETY: // - caller ensures there is no `&mut World` this makes it okay to make a `&World` - // - caller ensures there is no mutable borrows of world data, this means the caller cannot + // - caller ensures there are no mutable borrows of world data, this means the caller cannot // misuse the returned `&World` unsafe { self.unsafe_world() } } @@ -233,7 +234,7 @@ impl<'w> UnsafeWorldCell<'w> { /// /// # Safety /// - must not be used in a way that would conflict with any - /// live exclusive borrows on world data + /// live exclusive borrows of world data #[inline] unsafe fn unsafe_world(self) -> &'w World { // SAFETY: @@ -395,12 +396,12 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a reference to the resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures `self` has permission to access the resource // caller also ensure that no mutable reference to the resource exists unsafe { @@ -413,15 +414,15 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a reference including change detection to the resource of the given type if it exists. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource_ref(self) -> Option> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures `self` has permission to access the resource - // caller also ensure that no mutable reference to the resource exists + // caller also ensures that no mutable reference to the resource exists let (ptr, ticks, caller) = unsafe { self.get_resource_with_ticks(component_id)? }; // SAFETY: `component_id` was obtained from the type ID of `R` @@ -449,7 +450,7 @@ impl<'w> UnsafeWorldCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] @@ -465,12 +466,12 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a reference to the non-send resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_non_send_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures that `self` has permission to access `R` // caller ensures that no mutable reference exists to `R` unsafe { @@ -491,7 +492,7 @@ impl<'w> UnsafeWorldCell<'w> { /// This function will panic if it isn't called from the same thread that the resource was inserted from. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource /// - no mutable reference to the resource exists at the same time #[inline] @@ -507,13 +508,13 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a mutable reference to the resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] pub unsafe fn get_resource_mut(self) -> Option> { self.assert_allows_mutable_access(); - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: // - caller ensures `self` has permission to access the resource mutably // - caller ensures no other references to the resource exist @@ -532,7 +533,7 @@ impl<'w> UnsafeWorldCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] @@ -571,13 +572,13 @@ impl<'w> UnsafeWorldCell<'w> { /// Gets a mutable reference to the non-send resource of the given type if it exists /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] pub unsafe fn get_non_send_resource_mut(self) -> Option> { self.assert_allows_mutable_access(); - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: // - caller ensures that `self` has permission to access the resource // - caller ensures that the resource is unaliased @@ -599,7 +600,7 @@ impl<'w> UnsafeWorldCell<'w> { /// This function will panic if it isn't called from the same thread that the resource was inserted from. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no other references to the resource exist at the same time #[inline] @@ -633,7 +634,7 @@ impl<'w> UnsafeWorldCell<'w> { // Shorthand helper function for getting the data and change ticks for a resource. /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no mutable references to the resource exist at the same time #[inline] @@ -660,7 +661,7 @@ impl<'w> UnsafeWorldCell<'w> { /// This function will panic if it isn't called from the same thread that the resource was inserted from. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the resource mutably /// - no mutable references to the resource exist at the same time #[inline] @@ -684,7 +685,7 @@ impl<'w> UnsafeWorldCell<'w> { // Returns a mutable reference to the underlying world's [`CommandQueue`]. /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeWorldCell`] has permission to access the queue mutably /// - no mutable references to the queue exist at the same time pub(crate) unsafe fn get_raw_command_queue(self) -> RawCommandQueue { @@ -696,7 +697,7 @@ impl<'w> UnsafeWorldCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that there are no outstanding + /// It is the caller's responsibility to ensure that there are no outstanding /// references to `last_trigger_id`. pub(crate) unsafe fn increment_trigger_id(self) { self.assert_allows_mutable_access(); @@ -705,6 +706,18 @@ impl<'w> UnsafeWorldCell<'w> { (*self.ptr).last_trigger_id = (*self.ptr).last_trigger_id.wrapping_add(1); } } + + /// Convenience method for accessing the world's default error handler, + /// + /// # Safety + /// Must have read access to [`DefaultErrorHandler`]. + #[inline] + pub unsafe fn default_error_handler(&self) -> ErrorHandler { + self.get_resource::() + .copied() + .unwrap_or_default() + .0 + } } impl Debug for UnsafeWorldCell<'_> { @@ -714,7 +727,7 @@ impl Debug for UnsafeWorldCell<'_> { } } -/// A interior-mutable reference to a particular [`Entity`] and all of its components +/// An interior-mutable reference to a particular [`Entity`] and all of its components #[derive(Copy, Clone)] pub struct UnsafeEntityCell<'w> { world: UnsafeWorldCell<'w>, @@ -808,12 +821,12 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get(self) -> Option<&'w T> { - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct (T component_id + T::STORAGE_TYPE) // - `location` is valid @@ -832,14 +845,14 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get_ref(self) -> Option> { let last_change_tick = self.last_run; let change_tick = self.this_run; - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct (T component_id + T::STORAGE_TYPE) @@ -866,12 +879,12 @@ impl<'w> UnsafeEntityCell<'w> { /// detection in custom runtimes. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get_change_ticks(self) -> Option { - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - entity location is valid @@ -895,7 +908,7 @@ impl<'w> UnsafeEntityCell<'w> { /// compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] @@ -920,7 +933,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's 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] @@ -932,7 +945,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time /// - the component `T` is mutable @@ -943,7 +956,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time /// - The component `T` is mutable @@ -955,7 +968,7 @@ impl<'w> UnsafeEntityCell<'w> { ) -> Option> { self.world.assert_allows_mutable_access(); - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct @@ -982,10 +995,12 @@ impl<'w> UnsafeEntityCell<'w> { /// or `None` if the entity does not have the components required by the query `Q`. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the queried data immutably /// - no mutable references to the queried data exist at the same time - pub(crate) unsafe fn get_components(&self) -> Option> { + pub(crate) unsafe fn get_components( + &self, + ) -> Option> { // SAFETY: World is only used to access query data and initialize query state let state = unsafe { let world = self.world().world(); @@ -1015,7 +1030,8 @@ impl<'w> UnsafeEntityCell<'w> { // Table corresponds to archetype. State is the same state used to init fetch above. unsafe { Q::set_archetype(&mut fetch, &state, archetype, table) } // SAFETY: Called after set_archetype above. Entity and location are guaranteed to exist. - unsafe { Some(Q::fetch(&mut fetch, self.id(), location.table_row)) } + let item = unsafe { Q::fetch(&state, &mut fetch, self.id(), location.table_row) }; + Some(Q::release_state(item)) } else { None } @@ -1031,7 +1047,7 @@ impl<'w> UnsafeEntityCell<'w> { /// which is only valid while the `'w` borrow of the lifetime is active. /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component /// - no other mutable references to the component exist at the same time #[inline] @@ -1056,7 +1072,7 @@ impl<'w> UnsafeEntityCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's 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] @@ -1104,7 +1120,7 @@ impl<'w> UnsafeEntityCell<'w> { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// It is the callers responsibility to ensure that + /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the component mutably /// - no other references to the component exist at the same time /// - the component `T` is mutable diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index b2f1b92d82..9e05bc7a85 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_encase_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy derive macro for encase" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.17.0-dev" } encase_derive_impl = "0.10" [lints] diff --git a/crates/bevy_encase_derive/src/lib.rs b/crates/bevy_encase_derive/src/lib.rs index 15fbdca6a8..d882cb5cae 100644 --- a/crates/bevy_encase_derive/src/lib.rs +++ b/crates/bevy_encase_derive/src/lib.rs @@ -2,8 +2,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] use bevy_macro_utils::BevyManifest; diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index 864df285d9..afb20318bc 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -1,21 +1,21 @@ [package] name = "bevy_gilrs" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Gamepad system made using Gilrs for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index b9f1d9d286..7ec1c2e93b 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Systems and type definitions for gamepad handling in Bevy. @@ -15,7 +15,7 @@ mod gilrs_system; mod rumble; #[cfg(not(target_arch = "wasm32"))] -use bevy_utils::synccell::SyncCell; +use bevy_platform::cell::SyncCell; #[cfg(target_arch = "wasm32")] use core::cell::RefCell; @@ -39,7 +39,7 @@ thread_local! { /// `NonSendMut` parameter, which told Bevy that the system was `!Send`, but now with the removal of `!Send` /// resource/system parameter usage, there is no internal guarantee that the system will run in only one thread, so /// we need to rely on the platform to make such a guarantee. - static GILRS: RefCell> = const { RefCell::new(None) }; + pub static GILRS: RefCell> = const { RefCell::new(None) }; } #[derive(Resource)] @@ -47,6 +47,7 @@ pub(crate) struct Gilrs { #[cfg(not(target_arch = "wasm32"))] cell: SyncCell, } + impl Gilrs { #[inline] pub fn with(&mut self, f: impl FnOnce(&mut gilrs::Gilrs)) { diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 8f41a3ca22..b03fa69fe3 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -2,9 +2,9 @@ use crate::{Gilrs, GilrsGamepads}; use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource}; use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest}; +use bevy_platform::cell::SyncCell; use bevy_platform::collections::HashMap; use bevy_time::{Real, Time}; -use bevy_utils::synccell::SyncCell; use core::time::Duration; use gilrs::{ ff::{self, BaseEffect, BaseEffectType, Repeat, Replay}, diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 3a264c6244..c4833dbe7e 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_gizmos" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides gizmos for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -15,21 +15,21 @@ bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] [dependencies] # Bevy -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev", optional = true } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev", optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_gizmos_macros = { path = "macros", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev", optional = true } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev", optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_gizmos_macros = { path = "macros", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } # other bytemuck = "1.0" diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index b38a3c5374..b7effe24b0 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_gizmos_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_gizmos" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -13,7 +13,7 @@ proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 06a6a71f1f..e0f139a1a8 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -9,7 +9,8 @@ use core::{ use bevy_color::{Color, LinearRgba}; use bevy_ecs::{ - component::Tick, + component::{ComponentId, Tick}, + query::FilteredAccessSet, resource::Resource, system::{ Deferred, ReadOnlySystemParam, Res, SystemBuffer, SystemMeta, SystemParam, @@ -199,21 +200,24 @@ where type State = GizmosFetchState; type Item<'w, 's> = Gizmos<'w, 's, Config, Clear>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { GizmosFetchState { - state: GizmosState::::init_state(world, system_meta), + state: GizmosState::::init_state(world), } } - unsafe fn new_archetype( - state: &mut Self::State, - archetype: &bevy_ecs::archetype::Archetype, + fn init_access( + state: &Self::State, system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, ) { - // SAFETY: The caller ensures that `archetype` is from the World the state was initialized from in `init_state`. - unsafe { - GizmosState::::new_archetype(&mut state.state, archetype, system_meta); - }; + GizmosState::::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -222,12 +226,14 @@ where #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { // SAFETY: Delegated to existing `SystemParam` implementations. - unsafe { GizmosState::::validate_param(&state.state, system_meta, world) } + unsafe { + GizmosState::::validate_param(&mut state.state, system_meta, world) + } } #[inline] diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index cdcfc41236..2c85a0859d 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -172,6 +172,7 @@ where ); } } + impl GizmoBuffer where Config: GizmoConfigGroup, diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 581a30091d..5804729e06 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! This crate adds an immediate mode drawing api to Bevy for visual debugging. @@ -102,7 +102,7 @@ use crate::{config::ErasedGizmoConfigGroup, gizmos::GizmoBuffer}; #[cfg(feature = "bevy_render")] use { crate::retained::extract_linegizmos, - bevy_asset::{weak_handle, AssetId}, + bevy_asset::AssetId, bevy_ecs::{ component::Component, entity::Entity, @@ -119,8 +119,8 @@ use { render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::{ binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout, - BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, Shader, - ShaderStages, ShaderType, VertexFormat, + BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, ShaderStages, + ShaderType, VertexFormat, }, renderer::RenderDevice, sync_world::{MainEntity, TemporaryRenderEntity}, @@ -144,12 +144,6 @@ use gizmos::{GizmoStorage, Swap}; #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] use light::LightGizmoPlugin; -#[cfg(feature = "bevy_render")] -const LINE_SHADER_HANDLE: Handle = weak_handle!("15dc5869-ad30-4664-b35a-4137cb8804a1"); -#[cfg(feature = "bevy_render")] -const LINE_JOINT_SHADER_HANDLE: Handle = - weak_handle!("7b5bdda5-df81-4711-a6cf-e587700de6f2"); - /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. /// /// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`SpritePlugin`](bevy_sprite::SpritePlugin). @@ -160,14 +154,9 @@ impl Plugin for GizmoPlugin { fn build(&self, app: &mut App) { #[cfg(feature = "bevy_render")] { - use bevy_asset::load_internal_asset; - load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - LINE_JOINT_SHADER_HANDLE, - "line_joints.wgsl", - Shader::from_wgsl - ); + use bevy_asset::embedded_asset; + embedded_asset!(app, "lines.wgsl"); + embedded_asset!(app, "line_joints.wgsl"); } app.register_type::() @@ -642,8 +631,8 @@ impl RenderCommand

for SetLineGizmoBindGroup #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - uniform_index: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + uniform_index: Option>, bind_group: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -673,8 +662,8 @@ impl RenderCommand

for DrawLineGizmo #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -736,8 +725,8 @@ impl RenderCommand

for DrawLineJointGizmo { #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 15ed1c3ab0..a97071249d 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -2,9 +2,10 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, - SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, + SetLineGizmoBindGroup, }; use bevy_app::{App, Plugin}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ @@ -75,6 +76,7 @@ impl Plugin for LineGizmo2dPlugin { struct LineGizmoPipeline { mesh_pipeline: Mesh2dPipeline, uniform_layout: BindGroupLayout, + shader: Handle, } impl FromWorld for LineGizmoPipeline { @@ -85,6 +87,7 @@ impl FromWorld for LineGizmoPipeline { .resource::() .layout .clone(), + shader: load_embedded_asset!(render_world, "lines.wgsl"), } } } @@ -124,13 +127,13 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: line_gizmo_vertex_buffer_layouts(key.strip), }, fragment: Some(FragmentState { - shader: LINE_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: fragment_entry_point.into(), targets: vec![Some(ColorTargetState { @@ -173,6 +176,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { struct LineJointGizmoPipeline { mesh_pipeline: Mesh2dPipeline, uniform_layout: BindGroupLayout, + shader: Handle, } impl FromWorld for LineJointGizmoPipeline { @@ -183,6 +187,7 @@ impl FromWorld for LineJointGizmoPipeline { .resource::() .layout .clone(), + shader: load_embedded_asset!(render_world, "line_joints.wgsl"), } } } @@ -225,13 +230,13 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_JOINT_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: entry_point.into(), shader_defs: shader_defs.clone(), buffers: line_joint_gizmo_vertex_buffer_layouts(), }, fragment: Some(FragmentState { - shader: LINE_JOINT_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 87d865f8ca..1cc70c67cb 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -2,9 +2,10 @@ use crate::{ config::{GizmoLineJoint, GizmoLineStyle, GizmoMeshConfig}, line_gizmo_vertex_buffer_layouts, line_joint_gizmo_vertex_buffer_layouts, DrawLineGizmo, DrawLineJointGizmo, GizmoRenderSystems, GpuLineGizmo, LineGizmoUniformBindgroupLayout, - SetLineGizmoBindGroup, LINE_JOINT_SHADER_HANDLE, LINE_SHADER_HANDLE, + SetLineGizmoBindGroup, }; use bevy_app::{App, Plugin}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::{Transparent3d, CORE_3D_DEPTH_FORMAT}, oit::OrderIndependentTransparencySettings, @@ -75,6 +76,7 @@ impl Plugin for LineGizmo3dPlugin { struct LineGizmoPipeline { mesh_pipeline: MeshPipeline, uniform_layout: BindGroupLayout, + shader: Handle, } impl FromWorld for LineGizmoPipeline { @@ -85,6 +87,7 @@ impl FromWorld for LineGizmoPipeline { .resource::() .layout .clone(), + shader: load_embedded_asset!(render_world, "lines.wgsl"), } } } @@ -131,13 +134,13 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: line_gizmo_vertex_buffer_layouts(key.strip), }, fragment: Some(FragmentState { - shader: LINE_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: fragment_entry_point.into(), targets: vec![Some(ColorTargetState { @@ -171,6 +174,7 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { struct LineJointGizmoPipeline { mesh_pipeline: MeshPipeline, uniform_layout: BindGroupLayout, + shader: Handle, } impl FromWorld for LineJointGizmoPipeline { @@ -181,6 +185,7 @@ impl FromWorld for LineJointGizmoPipeline { .resource::() .layout .clone(), + shader: load_embedded_asset!(render_world, "line_joints.wgsl"), } } } @@ -230,13 +235,13 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: LINE_JOINT_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: entry_point.into(), shader_defs: shader_defs.clone(), buffers: line_joint_gizmo_vertex_buffer_layouts(), }, fragment: Some(FragmentState { - shader: LINE_JOINT_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index a67ab2276c..c46b74b7ca 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_gltf" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Bevy Engine GLTF loading" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -18,25 +18,25 @@ pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"] [dependencies] # bevy -bevy_animation = { path = "../bevy_animation", version = "0.16.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_scene = { path = "../bevy_scene", version = "0.16.0-dev", features = [ +bevy_animation = { path = "../bevy_animation", version = "0.17.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_scene = { path = "../bevy_scene", version = "0.17.0-dev", features = [ "bevy_render", ] } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -61,12 +61,12 @@ fixedbitset = "0.5" itertools = "0.14" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } -serde_json = "1" +serde_json = "1.0.140" smallvec = "1.11" tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } [lints] workspace = true diff --git a/crates/bevy_gltf/src/assets.rs b/crates/bevy_gltf/src/assets.rs index fe3303dd81..bfc920ebce 100644 --- a/crates/bevy_gltf/src/assets.rs +++ b/crates/bevy_gltf/src/assets.rs @@ -1,5 +1,7 @@ //! Representation of assets present in a glTF file +use core::ops::Deref; + #[cfg(feature = "bevy_animation")] use bevy_animation::AnimationClip; use bevy_asset::{Asset, Handle}; @@ -297,6 +299,21 @@ pub struct GltfMeshExtras { pub value: String, } +/// The mesh name of a glTF primitive. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component, Clone)] +pub struct GltfMeshName(pub String); + +impl Deref for GltfMeshName { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} + /// Additional untyped data that can be present on most glTF types at the material level. /// /// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). @@ -313,3 +330,11 @@ pub struct GltfMaterialExtras { #[derive(Clone, Debug, Reflect, Default, Component)] #[reflect(Component, Clone)] pub struct GltfMaterialName(pub String); + +impl Deref for GltfMaterialName { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.as_ref() + } +} diff --git a/crates/bevy_gltf/src/convert_coordinates.rs b/crates/bevy_gltf/src/convert_coordinates.rs new file mode 100644 index 0000000000..4148cecd9a --- /dev/null +++ b/crates/bevy_gltf/src/convert_coordinates.rs @@ -0,0 +1,80 @@ +use core::f32::consts::PI; + +use bevy_math::{Mat4, Quat, Vec3}; +use bevy_transform::components::Transform; + +pub(crate) trait ConvertCoordinates { + /// Converts the glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_coordinates(self) -> Self; +} + +pub(crate) trait ConvertCameraCoordinates { + /// Like `convert_coordinates`, but uses the following for the lens rotation: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_camera_coordinates(self) -> Self; +} + +impl ConvertCoordinates for Vec3 { + fn convert_coordinates(self) -> Self { + Vec3::new(-self.x, self.y, -self.z) + } +} + +impl ConvertCoordinates for [f32; 3] { + fn convert_coordinates(self) -> Self { + [-self[0], self[1], -self[2]] + } +} + +impl ConvertCoordinates for [f32; 4] { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + [-self[0], self[1], -self[2], self[3]] + } +} + +impl ConvertCoordinates for Quat { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + Quat::from_array([-self.x, self.y, -self.z, self.w]) + } +} + +impl ConvertCoordinates for Mat4 { + fn convert_coordinates(self) -> Self { + let m: Mat4 = Mat4::from_scale(Vec3::new(-1.0, 1.0, -1.0)); + // Same as the original matrix + let m_inv = m; + m_inv * self * m + } +} + +impl ConvertCoordinates for Transform { + fn convert_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotation = self.rotation.convert_coordinates(); + self + } +} + +impl ConvertCameraCoordinates for Transform { + fn convert_camera_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotate_y(PI); + self + } +} diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 02c14f4197..4262d43eb7 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Plugin providing an [`AssetLoader`](bevy_asset::AssetLoader) and type definitions @@ -91,6 +91,7 @@ //! You can use [`GltfAssetLabel`] to ensure you are using the correct label. mod assets; +mod convert_coordinates; mod label; mod loader; mod vertex_attributes; @@ -99,15 +100,15 @@ extern crate alloc; use alloc::sync::Arc; use std::sync::Mutex; +use tracing::warn; use bevy_platform::collections::HashMap; use bevy_app::prelude::*; use bevy_asset::AssetApp; use bevy_ecs::prelude::Resource; -use bevy_image::{CompressedImageFormats, ImageSamplerDescriptor}; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageSamplerDescriptor}; use bevy_mesh::MeshVertexAttribute; -use bevy_render::renderer::RenderDevice; /// The glTF prelude. /// @@ -193,6 +194,7 @@ impl Plugin for GltfPlugin { app.register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .init_asset::() @@ -204,10 +206,16 @@ impl Plugin for GltfPlugin { } fn finish(&self, app: &mut App) { - let supported_compressed_formats = match app.world().get_resource::() { - Some(render_device) => CompressedImageFormats::from_features(render_device.features()), - None => CompressedImageFormats::NONE, + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); + CompressedImageFormats::NONE }; + let default_sampler_resource = DefaultGltfImageSampler::new(&self.default_sampler); let default_sampler = default_sampler_resource.get_internal(); app.insert_resource(default_sampler_resource); diff --git a/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs index ef719891a4..60a153fed3 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs @@ -1,13 +1,17 @@ use bevy_mesh::PrimitiveTopology; -use gltf::mesh::{Mesh, Mode, Primitive}; +use gltf::{ + mesh::{Mesh, Mode}, + Material, +}; use crate::GltfError; -pub(crate) fn primitive_name(mesh: &Mesh<'_>, primitive: &Primitive) -> String { +pub(crate) fn primitive_name(mesh: &Mesh<'_>, material: &Material) -> String { let mesh_name = mesh.name().unwrap_or("Mesh"); - if mesh.primitives().len() > 1 { - format!("{}.{}", mesh_name, primitive.index()) + + if let Some(material_name) = material.name() { + format!("{}.{}", mesh_name, material_name) } else { mesh_name.to_string() } diff --git a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs index 83e6778b99..3fce51d527 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs @@ -10,7 +10,10 @@ use itertools::Itertools; #[cfg(feature = "bevy_animation")] use bevy_platform::collections::{HashMap, HashSet}; -use crate::GltfError; +use crate::{ + convert_coordinates::{ConvertCameraCoordinates as _, ConvertCoordinates as _}, + GltfError, +}; pub(crate) fn node_name(node: &Node) -> Name { let name = node @@ -26,8 +29,8 @@ pub(crate) fn node_name(node: &Node) -> Name { /// on [`Node::transform()`](gltf::Node::transform) directly because it uses optimized glam types and /// if `libm` feature of `bevy_math` crate is enabled also handles cross /// platform determinism properly. -pub(crate) fn node_transform(node: &Node) -> Transform { - match node.transform() { +pub(crate) fn node_transform(node: &Node, convert_coordinates: bool) -> Transform { + let transform = match node.transform() { gltf::scene::Transform::Matrix { matrix } => { Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) } @@ -40,6 +43,15 @@ pub(crate) fn node_transform(node: &Node) -> Transform { rotation: bevy_math::Quat::from_array(rotation), scale: Vec3::from(scale), }, + }; + if convert_coordinates { + if node.camera().is_some() { + transform.convert_camera_coordinates() + } else { + transform.convert_coordinates() + } + } else { + transform } } diff --git a/crates/bevy_gltf/src/loader/gltf_ext/texture.rs b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs index f666752479..0ea16936a6 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/texture.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs @@ -51,7 +51,7 @@ pub(crate) fn texture_sampler( // Shouldn't parse filters when anisotropic filtering is on, because trilinear is then required by wgpu. // We also trust user to have provided a valid sampler. - if sampler.anisotropy_clamp != 1 { + if sampler.anisotropy_clamp == 1 { if let Some(mag_filter) = gltf_sampler.mag_filter().map(|mf| match mf { MagFilter::Nearest => ImageFilterMode::Nearest, MagFilter::Linear => ImageFilterMode::Linear, diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index f85a739b2e..5e0f752e50 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -66,7 +66,7 @@ use tracing::{error, info_span, warn}; use crate::{ vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras, - GltfMaterialName, GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, + GltfMaterialName, GltfMeshExtras, GltfMeshName, GltfNode, GltfSceneExtras, GltfSkin, }; #[cfg(feature = "bevy_animation")] @@ -84,6 +84,7 @@ use self::{ texture::{texture_handle, texture_sampler, texture_transform_to_affine2}, }, }; +use crate::convert_coordinates::ConvertCoordinates as _; /// An error that occurs when loading a glTF file. #[derive(Error, Debug)] @@ -191,6 +192,16 @@ pub struct GltfLoaderSettings { pub default_sampler: Option, /// If true, the loader will ignore sampler data from gltf and use the default sampler. pub override_sampler: bool, + /// If true, the loader will convert glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub convert_coordinates: bool, } impl Default for GltfLoaderSettings { @@ -203,6 +214,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, + convert_coordinates: false, } } } @@ -303,7 +315,16 @@ 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(); + let translations: Vec = tr + .map(Vec3::from) + .map(|verts| { + if settings.convert_coordinates { + Vec3::convert_coordinates(verts) + } else { + verts + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( translation_property, @@ -350,8 +371,17 @@ async fn load_gltf<'a, 'b, 'c>( } ReadOutputs::Rotations(rots) => { let rotation_property = animated_field!(Transform::rotation); - let rotations: Vec = - rots.into_f32().map(Quat::from_array).collect(); + let rotations: Vec = rots + .into_f32() + .map(Quat::from_array) + .map(|quat| { + if settings.convert_coordinates { + Quat::convert_coordinates(quat) + } else { + quat + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( rotation_property, @@ -633,6 +663,7 @@ async fn load_gltf<'a, 'b, 'c>( accessor, &buffer_data, &loader.custom_vertex_attributes, + settings.convert_coordinates, ) { Ok((attribute, values)) => mesh.insert_attribute(attribute, values), Err(err) => warn!("{}", err), @@ -752,7 +783,17 @@ async fn load_gltf<'a, 'b, 'c>( let reader = gltf_skin.reader(|buffer| Some(&buffer_data[buffer.index()])); let local_to_bone_bind_matrices: Vec = reader .read_inverse_bind_matrices() - .map(|mats| mats.map(|mat| Mat4::from_cols_array_2d(&mat)).collect()) + .map(|mats| { + mats.map(|mat| Mat4::from_cols_array_2d(&mat)) + .map(|mat| { + if settings.convert_coordinates { + mat.convert_coordinates() + } else { + mat + } + }) + .collect() + }) .unwrap_or_else(|| { core::iter::repeat_n(Mat4::IDENTITY, gltf_skin.joints().len()).collect() }); @@ -834,7 +875,7 @@ async fn load_gltf<'a, 'b, 'c>( &node, children, mesh, - node_transform(&node), + node_transform(&node, settings.convert_coordinates), skin, node.extras().as_deref().map(GltfExtras::from), ); @@ -1043,71 +1084,75 @@ fn load_material( is_scale_inverted: bool, ) -> Handle { let material_label = material_label(material, is_scale_inverted); - load_context.labeled_asset_scope(material_label.to_string(), |load_context| { - let pbr = material.pbr_metallic_roughness(); + load_context + .labeled_asset_scope::<_, ()>(material_label.to_string(), |load_context| { + let pbr = material.pbr_metallic_roughness(); - // TODO: handle missing label handle errors here? - let color = pbr.base_color_factor(); - let base_color_channel = pbr - .base_color_texture() - .map(|info| uv_channel(material, "base color", info.tex_coord())) - .unwrap_or_default(); - let base_color_texture = pbr - .base_color_texture() - .map(|info| texture_handle(&info.texture(), load_context)); + // TODO: handle missing label handle errors here? + let color = pbr.base_color_factor(); + let base_color_channel = pbr + .base_color_texture() + .map(|info| uv_channel(material, "base color", info.tex_coord())) + .unwrap_or_default(); + let base_color_texture = pbr + .base_color_texture() + .map(|info| texture_handle(&info.texture(), load_context)); - let uv_transform = pbr - .base_color_texture() - .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) - .unwrap_or_default(); + let uv_transform = pbr + .base_color_texture() + .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) + .unwrap_or_default(); - let normal_map_channel = material - .normal_texture() - .map(|info| uv_channel(material, "normal map", info.tex_coord())) - .unwrap_or_default(); - let normal_map_texture: Option> = - material.normal_texture().map(|normal_texture| { - // TODO: handle normal_texture.scale - texture_handle(&normal_texture.texture(), load_context) + let normal_map_channel = material + .normal_texture() + .map(|info| uv_channel(material, "normal map", info.tex_coord())) + .unwrap_or_default(); + let normal_map_texture: Option> = + material.normal_texture().map(|normal_texture| { + // TODO: handle normal_texture.scale + texture_handle(&normal_texture.texture(), load_context) + }); + + let metallic_roughness_channel = pbr + .metallic_roughness_texture() + .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) + .unwrap_or_default(); + let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { + warn_on_differing_texture_transforms( + material, + &info, + uv_transform, + "metallic/roughness", + ); + texture_handle(&info.texture(), load_context) }); - let metallic_roughness_channel = pbr - .metallic_roughness_texture() - .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) - .unwrap_or_default(); - let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { - warn_on_differing_texture_transforms( - material, - &info, - uv_transform, - "metallic/roughness", - ); - texture_handle(&info.texture(), load_context) - }); + let occlusion_channel = material + .occlusion_texture() + .map(|info| uv_channel(material, "occlusion", info.tex_coord())) + .unwrap_or_default(); + let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { + // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + texture_handle(&occlusion_texture.texture(), load_context) + }); - let occlusion_channel = material - .occlusion_texture() - .map(|info| uv_channel(material, "occlusion", info.tex_coord())) - .unwrap_or_default(); - let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { - // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - texture_handle(&occlusion_texture.texture(), load_context) - }); + let emissive = material.emissive_factor(); + let emissive_channel = material + .emissive_texture() + .map(|info| uv_channel(material, "emissive", info.tex_coord())) + .unwrap_or_default(); + let emissive_texture = material.emissive_texture().map(|info| { + // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); + texture_handle(&info.texture(), load_context) + }); - let emissive = material.emissive_factor(); - let emissive_channel = material - .emissive_texture() - .map(|info| uv_channel(material, "emissive", info.tex_coord())) - .unwrap_or_default(); - let emissive_texture = material.emissive_texture().map(|info| { - // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); - texture_handle(&info.texture(), load_context) - }); - - #[cfg(feature = "pbr_transmission_textures")] - let (specular_transmission, specular_transmission_channel, specular_transmission_texture) = - material + #[cfg(feature = "pbr_transmission_textures")] + let ( + specular_transmission, + specular_transmission_channel, + specular_transmission_texture, + ) = material .transmission() .map_or((0.0, UvChannel::Uv0, None), |transmission| { let specular_transmission_channel = transmission @@ -1127,152 +1172,156 @@ fn load_material( ) }); - #[cfg(not(feature = "pbr_transmission_textures"))] - let specular_transmission = material - .transmission() - .map_or(0.0, |transmission| transmission.transmission_factor()); + #[cfg(not(feature = "pbr_transmission_textures"))] + let specular_transmission = material + .transmission() + .map_or(0.0, |transmission| transmission.transmission_factor()); - #[cfg(feature = "pbr_transmission_textures")] - let ( - thickness, - thickness_channel, - thickness_texture, - attenuation_distance, - attenuation_color, - ) = material.volume().map_or( - (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), - |volume| { - let thickness_channel = volume - .thickness_texture() - .map(|info| uv_channel(material, "thickness", info.tex_coord())) - .unwrap_or_default(); - let thickness_texture: Option> = - volume.thickness_texture().map(|thickness_texture| { - texture_handle(&thickness_texture.texture(), load_context) - }); + #[cfg(feature = "pbr_transmission_textures")] + let ( + thickness, + thickness_channel, + thickness_texture, + attenuation_distance, + attenuation_color, + ) = material.volume().map_or( + (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), + |volume| { + let thickness_channel = volume + .thickness_texture() + .map(|info| uv_channel(material, "thickness", info.tex_coord())) + .unwrap_or_default(); + let thickness_texture: Option> = + volume.thickness_texture().map(|thickness_texture| { + texture_handle(&thickness_texture.texture(), load_context) + }); - ( - volume.thickness_factor(), - thickness_channel, - thickness_texture, - volume.attenuation_distance(), - volume.attenuation_color(), - ) - }, - ); - - #[cfg(not(feature = "pbr_transmission_textures"))] - let (thickness, attenuation_distance, attenuation_color) = - material - .volume() - .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { ( volume.thickness_factor(), + thickness_channel, + thickness_texture, volume.attenuation_distance(), volume.attenuation_color(), ) - }); + }, + ); - let ior = material.ior().unwrap_or(1.5); + #[cfg(not(feature = "pbr_transmission_textures"))] + let (thickness, attenuation_distance, attenuation_color) = + material + .volume() + .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { + ( + volume.thickness_factor(), + volume.attenuation_distance(), + volume.attenuation_color(), + ) + }); - // Parse the `KHR_materials_clearcoat` extension data if necessary. - let clearcoat = - ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); + let ior = material.ior().unwrap_or(1.5); - // Parse the `KHR_materials_anisotropy` extension data if necessary. - let anisotropy = - AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_clearcoat` extension data if necessary. + let clearcoat = + ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); - // Parse the `KHR_materials_specular` extension data if necessary. - let specular = - SpecularExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_anisotropy` extension data if necessary. + let anisotropy = + AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); - // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels - let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); - let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + // Parse the `KHR_materials_specular` extension data if necessary. + let specular = + SpecularExtension::parse(load_context, document, material).unwrap_or_default(); - StandardMaterial { - base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), - base_color_channel, - base_color_texture, - perceptual_roughness: pbr.roughness_factor(), - metallic: pbr.metallic_factor(), - metallic_roughness_channel, - metallic_roughness_texture, - normal_map_channel, - normal_map_texture, - double_sided: material.double_sided(), - cull_mode: if material.double_sided() { - None - } else if is_scale_inverted { - Some(Face::Front) - } else { - Some(Face::Back) - }, - occlusion_channel, - occlusion_texture, - emissive, - emissive_channel, - emissive_texture, - specular_transmission, - #[cfg(feature = "pbr_transmission_textures")] - specular_transmission_channel, - #[cfg(feature = "pbr_transmission_textures")] - specular_transmission_texture, - thickness, - #[cfg(feature = "pbr_transmission_textures")] - thickness_channel, - #[cfg(feature = "pbr_transmission_textures")] - thickness_texture, - ior, - attenuation_distance, - attenuation_color: Color::linear_rgb( - attenuation_color[0], - attenuation_color[1], - attenuation_color[2], - ), - unlit: material.unlit(), - alpha_mode: alpha_mode(material), - uv_transform, - clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, - clearcoat_perceptual_roughness: clearcoat.clearcoat_roughness_factor.unwrap_or_default() - as f32, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_channel: clearcoat.clearcoat_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_texture: clearcoat.clearcoat_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, - anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, - anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_channel: anisotropy.anisotropy_channel, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_texture: anisotropy.anisotropy_texture, - // From the `KHR_materials_specular` spec: - // - reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, - #[cfg(feature = "pbr_specular_textures")] - specular_channel: specular.specular_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_texture: specular.specular_texture, - specular_tint: match specular.specular_color_factor { - Some(color) => Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32), - None => Color::WHITE, - }, - #[cfg(feature = "pbr_specular_textures")] - specular_tint_channel: specular.specular_color_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_tint_texture: specular.specular_color_texture, - ..Default::default() - } - }) + // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels + let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); + let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + + Ok(StandardMaterial { + base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), + base_color_channel, + base_color_texture, + perceptual_roughness: pbr.roughness_factor(), + metallic: pbr.metallic_factor(), + metallic_roughness_channel, + metallic_roughness_texture, + normal_map_channel, + normal_map_texture, + double_sided: material.double_sided(), + cull_mode: if material.double_sided() { + None + } else if is_scale_inverted { + Some(Face::Front) + } else { + Some(Face::Back) + }, + occlusion_channel, + occlusion_texture, + emissive, + emissive_channel, + emissive_texture, + specular_transmission, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture, + thickness, + #[cfg(feature = "pbr_transmission_textures")] + thickness_channel, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture, + ior, + attenuation_distance, + attenuation_color: Color::linear_rgb( + attenuation_color[0], + attenuation_color[1], + attenuation_color[2], + ), + unlit: material.unlit(), + alpha_mode: alpha_mode(material), + uv_transform, + clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, + clearcoat_perceptual_roughness: clearcoat + .clearcoat_roughness_factor + .unwrap_or_default() as f32, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: clearcoat.clearcoat_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: clearcoat.clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, + anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, + anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_channel: anisotropy.anisotropy_channel, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_texture: anisotropy.anisotropy_texture, + // From the `KHR_materials_specular` spec: + // + reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: specular.specular_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: specular.specular_texture, + specular_tint: match specular.specular_color_factor { + Some(color) => { + Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32) + } + None => Color::WHITE, + }, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: specular.specular_color_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: specular.specular_color_texture, + ..Default::default() + }) + }) + .unwrap() } /// Loads a glTF node. @@ -1298,7 +1347,7 @@ fn load_node( document: &Document, ) -> Result<(), GltfError> { let mut gltf_error = None; - let transform = node_transform(gltf_node); + let transform = node_transform(gltf_node, settings.convert_coordinates); let world_transform = *parent_transform * transform; // according to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#instantiation, // if the determinant of the transform is negative we must invert the winding order of @@ -1351,7 +1400,6 @@ fn load_node( }, ..OrthographicProjection::default_3d() }; - Projection::Orthographic(orthographic_projection) } gltf::camera::Projection::Perspective(perspective) => { @@ -1369,6 +1417,7 @@ fn load_node( Projection::Perspective(perspective_projection) } }; + node.insert(( Camera3d::default(), projection, @@ -1463,11 +1512,16 @@ fn load_node( }); } - if let Some(name) = material.name() { - mesh_entity.insert(GltfMaterialName(String::from(name))); + if let Some(name) = mesh.name() { + mesh_entity.insert(GltfMeshName(name.to_string())); } - mesh_entity.insert(Name::new(primitive_name(&mesh, &primitive))); + if let Some(name) = material.name() { + mesh_entity.insert(GltfMaterialName(name.to_string())); + } + + mesh_entity.insert(Name::new(primitive_name(&mesh, &material))); + // Mark for adding skinned mesh if let Some(skin) = gltf_node.skin() { entity_to_skin_index_map.insert(mesh_entity.id(), skin.index()); diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index d4ae811c90..2620d608a0 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -6,6 +6,8 @@ use gltf::{ }; use thiserror::Error; +use crate::convert_coordinates::ConvertCoordinates; + /// Represents whether integer data requires normalization #[derive(Copy, Clone)] struct Normalization(bool); @@ -132,15 +134,23 @@ impl<'a> VertexAttributeIter<'a> { } /// Materializes values for any supported format of vertex attribute - fn into_any_values(self) -> Result { + fn into_any_values(self, convert_coordinates: bool) -> Result { match self { VertexAttributeIter::F32(it) => Ok(Values::Float32(it.collect())), VertexAttributeIter::U32(it) => Ok(Values::Uint32(it.collect())), VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())), VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())), - VertexAttributeIter::F32x3(it) => Ok(Values::Float32x3(it.collect())), + VertexAttributeIter::F32x3(it) => Ok(if convert_coordinates { + Values::Float32x3(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x3(it.collect()) + }), VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())), - VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())), + VertexAttributeIter::F32x4(it) => Ok(if convert_coordinates { + Values::Float32x4(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x4(it.collect()) + }), VertexAttributeIter::U32x4(it) => Ok(Values::Uint32x4(it.collect())), VertexAttributeIter::S16x2(it, n) => { Ok(n.apply_either(it.collect(), Values::Snorm16x2, Values::Sint16x2)) @@ -188,7 +198,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4( ReadColors::RgbaU16(it).into_rgba_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -198,7 +208,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U8x4(it, Normalization(false)) => { Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -211,7 +221,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => { Ok(Values::Float32x4(ReadWeights::U16(it).into_f32().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -224,7 +234,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2( ReadTexCoords::U16(it).into_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } } @@ -252,28 +262,49 @@ pub(crate) fn convert_attribute( accessor: gltf::Accessor, buffer_data: &Vec>, custom_vertex_attributes: &HashMap, MeshVertexAttribute>, + convert_coordinates: bool, ) -> Result<(MeshVertexAttribute, Values), ConvertAttributeError> { - if let Some((attribute, conversion)) = match &semantic { - gltf::Semantic::Positions => Some((Mesh::ATTRIBUTE_POSITION, ConversionMode::Any)), - gltf::Semantic::Normals => Some((Mesh::ATTRIBUTE_NORMAL, ConversionMode::Any)), - gltf::Semantic::Tangents => Some((Mesh::ATTRIBUTE_TANGENT, ConversionMode::Any)), - gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba)), - gltf::Semantic::TexCoords(0) => Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord)), - gltf::Semantic::TexCoords(1) => Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord)), - gltf::Semantic::Joints(0) => { - Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex)) + if let Some((attribute, conversion, convert_coordinates)) = match &semantic { + gltf::Semantic::Positions => Some(( + Mesh::ATTRIBUTE_POSITION, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Normals => Some(( + Mesh::ATTRIBUTE_NORMAL, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Tangents => Some(( + Mesh::ATTRIBUTE_TANGENT, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba, false)), + gltf::Semantic::TexCoords(0) => { + Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord, false)) } - gltf::Semantic::Weights(0) => { - Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::JointWeight)) + gltf::Semantic::TexCoords(1) => { + Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord, false)) } + gltf::Semantic::Joints(0) => Some(( + Mesh::ATTRIBUTE_JOINT_INDEX, + ConversionMode::JointIndex, + false, + )), + gltf::Semantic::Weights(0) => Some(( + Mesh::ATTRIBUTE_JOINT_WEIGHT, + ConversionMode::JointWeight, + false, + )), gltf::Semantic::Extras(name) => custom_vertex_attributes .get(name.as_str()) - .map(|attr| (*attr, ConversionMode::Any)), + .map(|attr| (*attr, ConversionMode::Any, false)), _ => None, } { let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data); let converted_values = raw_iter.and_then(|iter| match conversion { - ConversionMode::Any => iter.into_any_values(), + ConversionMode::Any => iter.into_any_values(convert_coordinates), ConversionMode::Rgba => iter.into_rgba_values(), ConversionMode::TexCoord => iter.into_tex_coord_values(), ConversionMode::JointIndex => iter.into_joint_index_values(), diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 988325c707..c26a9da1bb 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_image" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides image types for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -32,7 +32,7 @@ qoi = ["image/qoi"] tga = ["image/tga"] tiff = ["image/tiff"] webp = ["image/webp"] -serialize = ["bevy_reflect", "bevy_platform/serialize", "bevy_utils/serde"] +serialize = ["bevy_reflect", "bevy_platform/serialize"] # For ktx2 supercompression zlib = ["flate2"] @@ -40,16 +40,17 @@ zstd = ["ruzstd"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ "serialize", "wgpu-types", ] } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } @@ -76,7 +77,7 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } half = { version = "2.4.1" } [dev-dependencies] -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } [lints] workspace = true diff --git a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs index e8b812194a..3e1c8fd542 100644 --- a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs @@ -5,8 +5,11 @@ use guillotiere::{size2, Allocation, AtlasAllocator}; use thiserror::Error; use tracing::error; +/// An error produced by [`DynamicTextureAtlasBuilder`] when trying to add a new +/// texture to a [`TextureAtlasLayout`]. #[derive(Debug, Error)] pub enum DynamicTextureAtlasBuilderError { + /// Unable to allocate space within the atlas for the new texture #[error("Couldn't allocate space to add the image requested")] FailedToAllocateSpace, /// Attempted to add a texture to an uninitialized atlas diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 5260c70bfc..e7548bb2bd 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -11,18 +11,21 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; +use bevy_ecs::resource::Resource; use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; use core::hash::Hash; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tracing::warn; use wgpu_types::{ AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor, SamplerDescriptor, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor, }; +/// Trait used to provide default values for Bevy-external types that +/// do not implement [`Default`]. pub trait BevyDefault { + /// Returns the default value for a type. fn bevy_default() -> Self; } @@ -358,7 +361,7 @@ pub struct Image { /// Used in [`Image`], this determines what image sampler to use when rendering. The default setting, /// [`ImageSampler::Default`], will read the sampler from the `ImagePlugin` at setup. /// Setting this to [`ImageSampler::Descriptor`] will override the global default descriptor for this [`Image`]. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub enum ImageSampler { /// Default image sampler, derived from the `ImagePlugin` setup. #[default] @@ -403,7 +406,7 @@ impl ImageSampler { /// See [`ImageSamplerDescriptor`] for information how to configure this. /// /// This type mirrors [`AddressMode`]. -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum ImageAddressMode { /// Clamp the value to the edge of the texture. /// @@ -432,7 +435,7 @@ pub enum ImageAddressMode { /// Texel mixing mode when sampling between texels. /// /// This type mirrors [`FilterMode`]. -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)] pub enum ImageFilterMode { /// Nearest neighbor sampling. /// @@ -448,7 +451,7 @@ pub enum ImageFilterMode { /// Comparison function used for depth and stencil operations. /// /// This type mirrors [`CompareFunction`]. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ImageCompareFunction { /// Function never passes Never, @@ -475,7 +478,7 @@ pub enum ImageCompareFunction { /// Color variation to use when the sampler addressing mode is [`ImageAddressMode::ClampToBorder`]. /// /// This type mirrors [`SamplerBorderColor`]. -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ImageSamplerBorderColor { /// RGBA color `[0, 0, 0, 0]`. TransparentBlack, @@ -498,7 +501,7 @@ pub enum ImageSamplerBorderColor { /// a breaking change. /// /// This types mirrors [`SamplerDescriptor`], but that might change in future versions. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ImageSamplerDescriptor { pub label: Option, /// How to deal with out of bounds accesses in the u (i.e. x) direction. @@ -850,7 +853,9 @@ impl Image { } /// Resizes the image to the new size, by removing information or appending 0 to the `data`. - /// Does not properly resize the contents of the image, but only its internal `data` buffer. + /// Does not properly scale the contents of the image. + /// + /// If you need to keep pixel data intact, use [`Image::resize_in_place`]. pub fn resize(&mut self, size: Extent3d) { self.texture_descriptor.size = size; if let Some(ref mut data) = self.data { @@ -858,8 +863,6 @@ impl Image { size.volume() * self.texture_descriptor.format.pixel_size(), 0, ); - } else { - warn!("Resized an uninitialized image. Directly modify image.texture_descriptor.size instead"); } } @@ -880,6 +883,52 @@ impl Image { self.texture_descriptor.size = new_size; } + /// Resizes the image to the new size, keeping the pixel data intact, anchored at the top-left. + /// When growing, the new space is filled with 0. When shrinking, the image is clipped. + /// + /// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`]. + pub fn resize_in_place(&mut self, new_size: Extent3d) -> Result<(), ResizeError> { + let old_size = self.texture_descriptor.size; + let pixel_size = self.texture_descriptor.format.pixel_size(); + let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume(); + + let Some(ref mut data) = self.data else { + return Err(ResizeError::ImageWithoutData); + }; + + let mut new: Vec = vec![0; byte_len]; + + let copy_width = old_size.width.min(new_size.width) as usize; + let copy_height = old_size.height.min(new_size.height) as usize; + let copy_depth = old_size + .depth_or_array_layers + .min(new_size.depth_or_array_layers) as usize; + + let old_row_stride = old_size.width as usize * pixel_size; + let old_layer_stride = old_size.height as usize * old_row_stride; + + let new_row_stride = new_size.width as usize * pixel_size; + let new_layer_stride = new_size.height as usize * new_row_stride; + + for z in 0..copy_depth { + for y in 0..copy_height { + let old_offset = z * old_layer_stride + y * old_row_stride; + let new_offset = z * new_layer_stride + y * new_row_stride; + + let old_range = (old_offset)..(old_offset + copy_width * pixel_size); + let new_range = (new_offset)..(new_offset + copy_width * pixel_size); + + new[new_range].copy_from_slice(&data[old_range]); + } + } + + self.data = Some(new); + + self.texture_descriptor.size = new_size; + + Ok(()) + } + /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets /// it as a 2D array texture, where each of the stacked images becomes one layer of the /// array. This is primarily for use with the `texture2DArray` shader uniform type. @@ -1542,6 +1591,14 @@ pub enum TextureError { IncompleteCubemap, } +/// An error that occurs when an image cannot be resized. +#[derive(Error, Debug)] +pub enum ResizeError { + /// Failed to resize an Image because it has no data. + #[error("resize method requires cpu-side image data but none was present")] + ImageWithoutData, +} + /// The type of a raw image buffer. #[derive(Debug)] pub enum ImageType<'a> { @@ -1651,6 +1708,12 @@ impl CompressedImageFormats { } } +/// For defining which compressed image formats are supported. This will be initialized from available device features +/// in `finish()` of the bevy `RenderPlugin`, but is left for the user to specify if not using the `RenderPlugin`, or +/// the WGPU backend. +#[derive(Resource)] +pub struct CompressedImageFormatSupport(pub CompressedImageFormats); + #[cfg(test)] mod test { use super::*; @@ -1726,4 +1789,173 @@ mod test { image.set_color_at_3d(4, 9, 2, Color::WHITE).unwrap(); assert!(matches!(image.get_color_at_3d(4, 9, 2), Ok(Color::WHITE))); } + + #[test] + fn resize_in_place_2d_grow_and_shrink() { + use bevy_color::ColorToPacked; + + const INITIAL_FILL: LinearRgba = LinearRgba::BLACK; + const GROW_FILL: LinearRgba = LinearRgba::NONE; + + let mut image = Image::new_fill( + Extent3d { + width: 2, + height: 2, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &INITIAL_FILL.to_u8_array(), + TextureFormat::Rgba8Unorm, + RenderAssetUsages::MAIN_WORLD, + ); + + // Create a test pattern + + const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [ + (0, 1, LinearRgba::RED), + (1, 1, LinearRgba::GREEN), + (1, 0, LinearRgba::BLUE), + ]; + + for (x, y, color) in &TEST_PIXELS { + image.set_color_at(*x, *y, Color::from(*color)).unwrap(); + } + + // Grow image + image + .resize_in_place(Extent3d { + width: 4, + height: 4, + depth_or_array_layers: 1, + }) + .unwrap(); + + // After growing, the test pattern should be the same. + assert!(matches!( + image.get_color_at(0, 0), + Ok(Color::LinearRgba(INITIAL_FILL)) + )); + for (x, y, color) in &TEST_PIXELS { + assert_eq!( + image.get_color_at(*x, *y).unwrap(), + Color::LinearRgba(*color) + ); + } + + // Pixels in the newly added area should get filled with zeroes. + assert!(matches!( + image.get_color_at(3, 3), + Ok(Color::LinearRgba(GROW_FILL)) + )); + + // Shrink + image + .resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }) + .unwrap(); + + // Images outside of the new dimensions should be clipped + assert!(image.get_color_at(1, 1).is_err()); + } + + #[test] + fn resize_in_place_array_grow_and_shrink() { + use bevy_color::ColorToPacked; + + const INITIAL_FILL: LinearRgba = LinearRgba::BLACK; + const GROW_FILL: LinearRgba = LinearRgba::NONE; + const LAYERS: u32 = 4; + + let mut image = Image::new_fill( + Extent3d { + width: 2, + height: 2, + depth_or_array_layers: LAYERS, + }, + TextureDimension::D2, + &INITIAL_FILL.to_u8_array(), + TextureFormat::Rgba8Unorm, + RenderAssetUsages::MAIN_WORLD, + ); + + // Create a test pattern + + const TEST_PIXELS: [(u32, u32, LinearRgba); 3] = [ + (0, 1, LinearRgba::RED), + (1, 1, LinearRgba::GREEN), + (1, 0, LinearRgba::BLUE), + ]; + + for z in 0..LAYERS { + for (x, y, color) in &TEST_PIXELS { + image + .set_color_at_3d(*x, *y, z, Color::from(*color)) + .unwrap(); + } + } + + // Grow image + image + .resize_in_place(Extent3d { + width: 4, + height: 4, + depth_or_array_layers: LAYERS + 1, + }) + .unwrap(); + + // After growing, the test pattern should be the same. + assert!(matches!( + image.get_color_at(0, 0), + Ok(Color::LinearRgba(INITIAL_FILL)) + )); + for z in 0..LAYERS { + for (x, y, color) in &TEST_PIXELS { + assert_eq!( + image.get_color_at_3d(*x, *y, z).unwrap(), + Color::LinearRgba(*color) + ); + } + } + + // Pixels in the newly added area should get filled with zeroes. + for z in 0..(LAYERS + 1) { + assert!(matches!( + image.get_color_at_3d(3, 3, z), + Ok(Color::LinearRgba(GROW_FILL)) + )); + } + + // Shrink + image + .resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }) + .unwrap(); + + // Images outside of the new dimensions should be clipped + assert!(image.get_color_at_3d(1, 1, 0).is_err()); + + // Higher layers should no longer be present + assert!(image.get_color_at_3d(0, 0, 1).is_err()); + + // Grow layers + image + .resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 2, + }) + .unwrap(); + + // Pixels in the newly added layer should be zeroes. + assert!(matches!( + image.get_color_at_3d(0, 0, 1), + Ok(Color::LinearRgba(GROW_FILL)) + )); + } } diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 0ef1213b46..fe086db674 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -81,19 +81,35 @@ impl ImageLoader { } } +/// How to determine an image's format when loading. #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub enum ImageFormatSetting { + /// Determine the image format from its file extension. + /// + /// This is the default. #[default] FromExtension, + /// Declare the image format explicitly. Format(ImageFormat), + /// Guess the image format by looking for magic bytes at the + /// beginning of its data. Guess, } +/// Settings for loading an [`Image`] using an [`ImageLoader`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageLoaderSettings { + /// How to determine the image's format. pub format: ImageFormatSetting, + /// Specifies whether image data is linear + /// or in sRGB space when this is not determined by + /// the image format. pub is_srgb: bool, + /// [`ImageSampler`] to use when rendering - this does + /// not affect the loading of the image data. pub sampler: ImageSampler, + /// Where the asset will be used - see the docs on + /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, } @@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings { } } +/// An error when loading an image using [`ImageLoader`]. #[non_exhaustive] #[derive(Debug, Error)] pub enum ImageLoaderError { - #[error("Could load shader: {0}")] + /// An error occurred while trying to load the image bytes. + #[error("Failed to load image bytes: {0}")] Io(#[from] std::io::Error), + /// An error occurred while trying to decode the image bytes. #[error("Could not load texture file: {0}")] FileTexture(#[from] FileTextureError), } @@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader { /// An error that occurs when loading a texture from a file. #[derive(Error, Debug)] -#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")] +#[error("Error reading image file {path}: {error}.")] pub struct FileTextureError { error: TextureError, path: String, diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 0cccbacb07..c86e32ef52 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -267,6 +267,9 @@ pub fn ktx2_buffer_to_image( let mut image = Image::default(); image.texture_descriptor.format = texture_format; image.data = Some(wgpu_data.into_iter().flatten().collect::>()); + // Note: we must give wgpu the logical texture dimensions, so it can correctly compute mip sizes. + // However this currently causes wgpu to panic if the dimensions arent a multiple of blocksize. + // See https://github.com/gfx-rs/wgpu/issues/7677 for more context. image.texture_descriptor.size = Extent3d { width, height, @@ -276,8 +279,7 @@ pub fn ktx2_buffer_to_image( depth } .max(1), - } - .physical_size(texture_format); + }; image.texture_descriptor.mip_level_count = level_count; image.texture_descriptor.dimension = if depth > 1 { TextureDimension::D3 diff --git a/crates/bevy_image/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs index 4caeed8c07..67e1b20317 100644 --- a/crates/bevy_image/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -34,6 +34,7 @@ pub struct TextureAtlasSources { /// Maps from a specific image handle to the index in `textures` where they can be found. pub texture_ids: HashMap, usize>, } + impl TextureAtlasSources { /// Retrieves the texture *section* index of the given `texture` handle. pub fn texture_index(&self, texture: impl Into>) -> Option { diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index 570273a00a..2961c0d115 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_input" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides input functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -42,7 +42,6 @@ std = [ "bevy_app/std", "bevy_ecs/std", "bevy_math/std", - "bevy_utils/std", "bevy_reflect/std", "bevy_platform/std", ] @@ -61,14 +60,14 @@ libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "glam", ], default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1", features = [ diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index bc28381ab4..e4ff47f470 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -71,7 +71,7 @@ use { /// Reading and checking against the current set of pressed buttons: /// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, Update}; -/// # use bevy_ecs::{prelude::{IntoScheduleConfigs, Res, Resource, resource_changed}, schedule::Condition}; +/// # use bevy_ecs::{prelude::{IntoScheduleConfigs, Res, Resource, resource_changed}, schedule::SystemCondition}; /// # use bevy_input::{ButtonInput, prelude::{KeyCode, MouseButton}}; /// /// fn main() { @@ -122,7 +122,7 @@ use { /// [`DetectChangesMut::bypass_change_detection`]: bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection #[derive(Debug, Clone, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Resource))] -pub struct ButtonInput { +pub struct ButtonInput { /// A collection of every button that is currently being pressed. pressed: HashSet, /// A collection of every button that has just been pressed. @@ -131,7 +131,7 @@ pub struct ButtonInput { just_released: HashSet, } -impl Default for ButtonInput { +impl Default for ButtonInput { fn default() -> Self { Self { pressed: Default::default(), @@ -143,12 +143,12 @@ impl Default for ButtonInput { impl ButtonInput where - T: Copy + Eq + Hash + Send + Sync + 'static, + T: Clone + Eq + Hash + Send + Sync + 'static, { /// Registers a press for the given `input`. pub fn press(&mut self, input: T) { // Returns `true` if the `input` wasn't pressed. - if self.pressed.insert(input) { + if self.pressed.insert(input.clone()) { self.just_pressed.insert(input); } } diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 2b0148909c..7d2f551201 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, entity::Entity, - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, name::Name, system::{Commands, Query}, }; @@ -32,7 +32,7 @@ use thiserror::Error; /// the in-frame relative ordering of events is important. /// /// This event is produced by `bevy_input`. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -59,7 +59,7 @@ pub enum GamepadEvent { /// the in-frame relative ordering of events is important. /// /// This event type is used by `bevy_input` to feed its components. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -80,7 +80,7 @@ pub enum RawGamepadEvent { } /// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -112,7 +112,7 @@ impl RawGamepadButtonChangedEvent { } /// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -143,9 +143,9 @@ impl RawGamepadAxisChangedEvent { } } -/// A Gamepad connection event. Created when a connection to a gamepad +/// A [`Gamepad`] connection event. Created when a connection to a gamepad /// is established and when a gamepad is disconnected. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -184,7 +184,7 @@ impl GamepadConnectionEvent { } /// [`GamepadButton`] event triggered by a digital state change. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -216,7 +216,7 @@ impl GamepadButtonStateChangedEvent { } /// [`GamepadButton`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -251,7 +251,7 @@ impl GamepadButtonChangedEvent { } /// [`GamepadAxis`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( feature = "bevy_reflect", @@ -1774,7 +1774,7 @@ impl GamepadRumbleIntensity { #[doc(alias = "force feedback")] #[doc(alias = "vibration")] #[doc(alias = "vibrate")] -#[derive(Event, Clone)] +#[derive(Event, BufferedEvent, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone))] pub enum GamepadRumbleRequest { /// Add a rumble to the given gamepad. diff --git a/crates/bevy_input/src/gestures.rs b/crates/bevy_input/src/gestures.rs index 5cd14d4634..9daa21d525 100644 --- a/crates/bevy_input/src/gestures.rs +++ b/crates/bevy_input/src/gestures.rs @@ -1,6 +1,6 @@ //! Gestures functionality, from touchscreens and touchpads. -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_math::Vec2; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -17,7 +17,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -39,7 +39,7 @@ pub struct PinchGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -58,7 +58,7 @@ pub struct RotationGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -76,7 +76,7 @@ pub struct DoubleTapGesture; /// ## Platform-specific /// /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index ea5452fb53..70efe18a84 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -69,7 +69,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, system::ResMut, }; @@ -92,9 +92,10 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// ## Usage /// -/// The event is consumed inside of the [`keyboard_input_system`] -/// to update the [`ButtonInput`](ButtonInput) resource. -#[derive(Event, Debug, Clone, PartialEq, Eq, Hash)] +/// The event is consumed inside of the [`keyboard_input_system`] to update the +/// [`ButtonInput`](ButtonInput) and +/// [`ButtonInput`](ButtonInput) resources. +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -107,8 +108,12 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; )] pub struct KeyboardInput { /// The physical key code of the key. + /// + /// This corresponds to the location of the key independent of the keyboard layout. pub key_code: KeyCode, - /// The logical key of the input + /// The logical key of the input. + /// + /// This corresponds to the actual key taking keyboard layout into account. pub logical_key: Key, /// The press state of the key. pub state: ButtonState, @@ -139,7 +144,7 @@ pub struct KeyboardInput { /// when, for example, switching between windows with 'Alt-Tab' or using any other /// OS specific key combination that leads to Bevy window losing focus and not receiving any /// input events -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -148,32 +153,46 @@ pub struct KeyboardInput { )] pub struct KeyboardFocusLost; -/// Updates the [`ButtonInput`] resource with the latest [`KeyboardInput`] events. +/// Updates the [`ButtonInput`] and [`ButtonInput`] resources with the latest [`KeyboardInput`] events. /// /// ## Differences /// -/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources is that +/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources are that /// the latter has convenient functions such as [`ButtonInput::pressed`], [`ButtonInput::just_pressed`] and [`ButtonInput::just_released`] and is window id agnostic. +/// +/// There is a [`ButtonInput`] for both [`KeyCode`] and [`Key`] as they are both useful in different situations, see their documentation for the details. pub fn keyboard_input_system( - mut key_input: ResMut>, + mut keycode_input: ResMut>, + mut key_input: ResMut>, mut keyboard_input_events: EventReader, mut focus_events: EventReader, ) { - // Avoid clearing if it's not empty to ensure change detection is not triggered. + // Avoid clearing if not empty to ensure change detection is not triggered. + keycode_input.bypass_change_detection().clear(); key_input.bypass_change_detection().clear(); + for event in keyboard_input_events.read() { let KeyboardInput { - key_code, state, .. + key_code, + logical_key, + state, + .. } = event; match state { - ButtonState::Pressed => key_input.press(*key_code), - ButtonState::Released => key_input.release(*key_code), + ButtonState::Pressed => { + keycode_input.press(*key_code); + key_input.press(logical_key.clone()); + } + ButtonState::Released => { + keycode_input.release(*key_code); + key_input.release(logical_key.clone()); + } } } // Release all cached input to avoid having stuck input when switching between windows in os if !focus_events.is_empty() { - key_input.release_all(); + keycode_input.release_all(); focus_events.clear(); } } @@ -220,13 +239,13 @@ pub enum NativeKeyCode { /// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. /// /// Code representing the location of a physical key -/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// This mostly conforms to the [`UI Events Specification's KeyboardEvent.code`] with a few /// exceptions: /// - The keys that the specification calls `MetaLeft` and `MetaRight` are named `SuperLeft` and /// `SuperRight` here. /// - The key that the specification calls "Super" is reported as `Unidentified` here. /// -/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +/// [`UI Events Specification's KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables /// /// ## Updating /// @@ -756,6 +775,19 @@ pub enum NativeKey { /// The logical key code of a [`KeyboardInput`]. /// +/// This contains the actual value that is produced by pressing the key. This is +/// useful when you need the actual letters, and for symbols like `+` and `-` +/// when implementing zoom, as they can be in different locations depending on +/// the keyboard layout. +/// +/// In many cases you want the key location instead, for example when +/// implementing WASD controls so the keys are located the same place on QWERTY +/// and other layouts. In that case use [`KeyCode`] instead. +/// +/// ## Usage +/// +/// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. +/// /// ## Technical /// /// Its values map 1 to 1 to winit's Key. diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 67c8995179..5a6a177223 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -1,12 +1,12 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] -//! Input functionality for the [Bevy game engine](https://bevyengine.org/). +//! Input functionality for the [Bevy game engine](https://bevy.org/). //! //! # Supported input devices //! @@ -49,7 +49,7 @@ use bevy_ecs::prelude::*; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use gestures::*; -use keyboard::{keyboard_input_system, KeyCode, KeyboardFocusLost, KeyboardInput}; +use keyboard::{keyboard_input_system, Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use mouse::{ accumulate_mouse_motion_system, accumulate_mouse_scroll_system, mouse_button_input_system, AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton, MouseButtonInput, MouseMotion, @@ -89,6 +89,7 @@ impl Plugin for InputPlugin { .add_event::() .add_event::() .init_resource::>() + .init_resource::>() .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystems)) // mouse .add_event::() diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index 3a377d9329..e6b52bf51d 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -4,7 +4,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -26,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// The event is read inside of the [`mouse_button_input_system`] /// to update the [`ButtonInput`] resource. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -91,7 +91,7 @@ pub enum MouseButton { /// However, the event data does not make it possible to distinguish which device it is referring to. /// /// [`DeviceEvent::MouseMotion`]: https://docs.rs/winit/latest/winit/event/enum.DeviceEvent.html#variant.MouseMotion -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -140,7 +140,7 @@ pub enum MouseScrollUnit { /// A mouse wheel event. /// /// This event is the translated version of the `WindowEvent::MouseWheel` from the `winit` crate. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/touch.rs b/crates/bevy_input/src/touch.rs index 28f3159d53..df1cf3764f 100644 --- a/crates/bevy_input/src/touch.rs +++ b/crates/bevy_input/src/touch.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -37,7 +37,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// This event is the translated version of the `WindowEvent::Touch` from the `winit` crate. /// It is available to the end user and can be used for game logic. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index 0b2ca53830..60b824258d 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_input_focus" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Keyboard focus management" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -60,12 +60,13 @@ libm = ["bevy_math/libm", "bevy_window/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", default-features = false } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [ "glam", ], default-features = false, optional = true } @@ -73,9 +74,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ thiserror = { version = "2", default-features = false } log = { version = "0.4", default-features = false } -[dev-dependencies] -smol_str = "0.2" - [lints] workspace = true diff --git a/crates/bevy_input_focus/src/autofocus.rs b/crates/bevy_input_focus/src/autofocus.rs index 72024418d2..f6a862db88 100644 --- a/crates/bevy_input_focus/src/autofocus.rs +++ b/crates/bevy_input_focus/src/autofocus.rs @@ -1,6 +1,6 @@ //! Contains the [`AutoFocus`] component and related machinery. -use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld}; +use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld}; use crate::InputFocus; diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 44ff0ef645..df7690ef26 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -30,7 +30,7 @@ pub mod tab_navigation; mod autofocus; pub use autofocus::*; -use bevy_app::{App, Plugin, PreUpdate, Startup}; +use bevy_app::{App, Plugin, PostStartup, PreUpdate}; use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal}; use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel}; use bevy_window::{PrimaryWindow, Window}; @@ -137,19 +137,23 @@ pub struct InputFocusVisible(pub bool); /// /// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, /// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`]. -#[derive(Clone, Debug, Component)] +#[derive(Event, EntityEvent, Clone, Debug, Component)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] -pub struct FocusedInput { +pub struct FocusedInput { /// The underlying input event. pub input: E, /// The primary window entity. window: Entity, } -impl Event for FocusedInput { - type Traversal = WindowTraversal; - - const AUTO_PROPAGATE: bool = true; +/// An event which is used to set input focus. Trigger this on an entity, and it will bubble +/// until it finds a focusable entity, and then set focus to it. +#[derive(Clone, Event, EntityEvent)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] +pub struct AcquireFocus { + /// The primary window entity. + window: Entity, } #[derive(QueryData)] @@ -159,8 +163,26 @@ pub struct WindowTraversal { window: Option<&'static Window>, } -impl Traversal> for WindowTraversal { - fn traverse(item: Self::Item<'_>, event: &FocusedInput) -> Option { +impl Traversal> for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput) -> Option { + let WindowTraversalItem { child_of, window } = item; + + // Send event to parent, if it has one. + if let Some(child_of) = child_of { + return Some(child_of.parent()); + }; + + // Otherwise, send it to the window entity (unless this is a window entity). + if window.is_none() { + return Some(event.window); + } + + None + } +} + +impl Traversal for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option { let WindowTraversalItem { child_of, window } = item; // Send event to parent, if it has one. @@ -185,7 +207,7 @@ pub struct InputDispatchPlugin; impl Plugin for InputDispatchPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, set_initial_focus) + app.add_systems(PostStartup, set_initial_focus) .init_resource::() .init_resource::() .add_systems( @@ -218,17 +240,19 @@ pub enum InputFocusSystems { #[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")] pub type InputFocusSet = InputFocusSystems; -/// Sets the initial focus to the primary window, if any. +/// If no entity is focused, sets the focus to the primary window, if any. pub fn set_initial_focus( mut input_focus: ResMut, window: Single>, ) { - input_focus.0 = Some(*window); + if input_focus.0.is_none() { + input_focus.0 = Some(*window); + } } /// System which dispatches bubbled input events to the focused entity, or to the primary window /// if no entity has focus. -pub fn dispatch_focused_input( +pub fn dispatch_focused_input( mut key_events: EventReader, focus: Res, windows: Query>, @@ -368,30 +392,18 @@ mod tests { use super::*; use alloc::string::String; - use bevy_ecs::{ - component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, - }; + use bevy_app::Startup; + use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld}; use bevy_input::{ keyboard::{Key, KeyCode}, ButtonState, InputPlugin, }; - use bevy_window::WindowResolution; - use smol_str::SmolStr; - - #[derive(Component)] - #[component(on_add = set_focus_on_add)] - struct SetFocusOnAdd; - - fn set_focus_on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { - let mut input_focus = world.resource_mut::(); - input_focus.set(entity); - } #[derive(Component, Default)] struct GatherKeyboardEvents(String); fn gather_keyboard_events( - trigger: Trigger>, + trigger: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { if let Ok(mut gather) = query.get_mut(trigger.target()) { @@ -401,14 +413,16 @@ mod tests { } } - const KEY_A_EVENT: KeyboardInput = KeyboardInput { - key_code: KeyCode::KeyA, - logical_key: Key::Character(SmolStr::new_static("A")), - state: ButtonState::Pressed, - text: Some(SmolStr::new_static("A")), - repeat: false, - window: Entity::PLACEHOLDER, - }; + fn key_a_event() -> KeyboardInput { + KeyboardInput { + key_code: KeyCode::KeyA, + logical_key: Key::Character("A".into()), + state: ButtonState::Pressed, + text: Some("A".into()), + repeat: false, + window: Entity::PLACEHOLDER, + } + } #[test] fn test_no_panics_if_resource_missing() { @@ -438,6 +452,55 @@ mod tests { .unwrap(); } + #[test] + fn initial_focus_unset_if_no_primary_window() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + app.update(); + + assert_eq!(app.world().resource::().0, None); + } + + #[test] + fn initial_focus_set_to_primary_window() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + let entity_window = app + .world_mut() + .spawn((Window::default(), PrimaryWindow)) + .id(); + app.update(); + + assert_eq!(app.world().resource::().0, Some(entity_window)); + } + + #[test] + fn initial_focus_not_overridden() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + app.world_mut().spawn((Window::default(), PrimaryWindow)); + + app.add_systems(Startup, |mut commands: Commands| { + commands.spawn(AutoFocus); + }); + + app.update(); + + let autofocus_entity = app + .world_mut() + .query_filtered::>() + .single(app.world()) + .unwrap(); + + assert_eq!( + app.world().resource::().0, + Some(autofocus_entity) + ); + } + #[test] fn test_keyboard_events() { fn get_gathered(app: &App, entity: Entity) -> &str { @@ -454,18 +517,14 @@ mod tests { app.add_plugins((InputPlugin, InputDispatchPlugin)) .add_observer(gather_keyboard_events); - let window = Window { - resolution: WindowResolution::new(800., 600.), - ..Default::default() - }; - app.world_mut().spawn((window, PrimaryWindow)); + app.world_mut().spawn((Window::default(), PrimaryWindow)); // Run the world for a single frame to set up the initial focus app.update(); let entity_a = app .world_mut() - .spawn((GatherKeyboardEvents::default(), SetFocusOnAdd)) + .spawn((GatherKeyboardEvents::default(), AutoFocus)) .id(); let child_of_b = app @@ -487,7 +546,7 @@ mod tests { assert!(!app.world().is_focus_visible(child_of_b)); // entity_a should receive this event - app.world_mut().send_event(KEY_A_EVENT); + app.world_mut().send_event(key_a_event()); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); @@ -500,7 +559,7 @@ mod tests { assert!(!app.world().is_focus_visible(entity_a)); // This event should be lost - app.world_mut().send_event(KEY_A_EVENT); + app.world_mut().send_event(key_a_event()); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); @@ -520,7 +579,8 @@ mod tests { assert!(app.world().is_focus_within(entity_b)); // These events should be received by entity_b and child_of_b - app.world_mut().send_event_batch([KEY_A_EVENT; 4]); + app.world_mut() + .send_event_batch(core::iter::repeat_n(key_a_event(), 4)); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index a5fe691458..ef018c56fb 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -30,7 +30,7 @@ use bevy_ecs::{ component::Component, entity::Entity, hierarchy::{ChildOf, Children}, - observer::Trigger, + observer::On, query::{With, Without}, system::{Commands, Query, Res, ResMut, SystemParam}, }; @@ -38,11 +38,12 @@ use bevy_input::{ keyboard::{KeyCode, KeyboardInput}, ButtonInput, ButtonState, }; -use bevy_window::PrimaryWindow; +use bevy_picking::events::{Pointer, Press}; +use bevy_window::{PrimaryWindow, Window}; use log::warn; use thiserror::Error; -use crate::{FocusedInput, InputFocus, InputFocusVisible}; +use crate::{AcquireFocus, FocusedInput, InputFocus, InputFocusVisible}; #[cfg(feature = "bevy_reflect")] use { @@ -221,7 +222,7 @@ impl TabNavigation<'_, '_> { action: NavAction, ) -> Result { // List of all focusable entities found. - let mut focusable: Vec<(Entity, TabIndex)> = + let mut focusable: Vec<(Entity, TabIndex, usize)> = Vec::with_capacity(self.tabindex_query.iter().len()); match tabgroup { @@ -229,7 +230,7 @@ impl TabNavigation<'_, '_> { // We're in a modal tab group, then gather all tab indices in that group. if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) { for child in children.iter() { - self.gather_focusable(&mut focusable, *child); + self.gather_focusable(&mut focusable, *child, 0); } } } @@ -245,9 +246,12 @@ impl TabNavigation<'_, '_> { tab_groups.sort_by_key(|(_, tg)| tg.order); // Search group descendants - tab_groups.iter().for_each(|(tg_entity, _)| { - self.gather_focusable(&mut focusable, *tg_entity); - }); + tab_groups + .iter() + .enumerate() + .for_each(|(idx, (tg_entity, _))| { + self.gather_focusable(&mut focusable, *tg_entity, idx); + }); } } @@ -255,8 +259,14 @@ impl TabNavigation<'_, '_> { return Err(TabNavigationError::NoFocusableEntities); } - // Stable sort by tabindex - focusable.sort_by_key(|(_, idx)| *idx); + // Sort by TabGroup and then TabIndex + focusable.sort_by(|(_, a_tab_idx, a_group), (_, b_tab_idx, b_group)| { + if a_group == b_group { + a_tab_idx.cmp(b_tab_idx) + } else { + a_group.cmp(b_group) + } + }); let index = focusable.iter().position(|e| Some(e.0) == focus.0); let count = focusable.len(); @@ -267,37 +277,67 @@ impl TabNavigation<'_, '_> { (None, NavAction::Previous) | (_, NavAction::Last) => count - 1, }; match focusable.get(next) { - Some((entity, _)) => Ok(*entity), + Some((entity, _, _)) => Ok(*entity), None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity), } } /// Gather all focusable entities in tree order. - fn gather_focusable(&self, out: &mut Vec<(Entity, TabIndex)>, parent: Entity) { + fn gather_focusable( + &self, + out: &mut Vec<(Entity, TabIndex, usize)>, + parent: Entity, + tab_group_idx: usize, + ) { if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) { if let Some(tabindex) = tabindex { if tabindex.0 >= 0 { - out.push((entity, *tabindex)); + out.push((entity, *tabindex, tab_group_idx)); } } if let Some(children) = children { for child in children.iter() { // Don't traverse into tab groups, as they are handled separately. if self.tabgroup_query.get(*child).is_err() { - self.gather_focusable(out, *child); + self.gather_focusable(out, *child, tab_group_idx); } } } } else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent) { if !tabgroup.modal { for child in children.iter() { - self.gather_focusable(out, *child); + self.gather_focusable(out, *child, tab_group_idx); } } } } } +/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling. +pub(crate) fn acquire_focus( + mut ev: On, + focusable: Query<(), With>, + windows: Query<(), With>, + mut focus: ResMut, +) { + // If the entity has a TabIndex + if focusable.contains(ev.target()) { + // Stop and focus it + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0 != Some(ev.target()) { + focus.0 = Some(ev.target()); + } + } else if windows.contains(ev.target()) { + // Stop and clear focus + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0.is_some() { + focus.clear(); + } + } +} + /// Plugin for navigating between focusable entities using keyboard input. pub struct TabNavigationPlugin; @@ -307,6 +347,8 @@ impl Plugin for TabNavigationPlugin { #[cfg(feature = "bevy_reflect")] app.register_type::().register_type::(); + app.add_observer(acquire_focus); + app.add_observer(click_to_focus); } } @@ -316,6 +358,30 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut focus_visible: ResMut, + windows: Query>, + mut commands: Commands, +) { + // Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event + // for every ancestor, but only for the original entity. Also, users may want to stop + // propagation on the pointer event at some point along the bubbling chain, so we need our + // own dedicated event whose propagation we can control. + if ev.target() == ev.original_target() { + // Clicking hides focus + if focus_visible.0 { + focus_visible.0 = false; + } + // Search for a focusable parent entity, defaulting to window if none. + if let Ok(window) = windows.single() { + commands + .entity(ev.target()) + .trigger(AcquireFocus { window }); + } + } +} + /// Observer function which handles tab navigation. /// /// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events, @@ -323,7 +389,7 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut trigger: On>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut, @@ -397,4 +463,45 @@ mod tests { let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last); assert_eq!(last_entity, Ok(tab_entity_2)); } + + #[test] + fn test_tab_navigation_between_groups_is_sorted_by_group() { + let mut app = App::new(); + let world = app.world_mut(); + + let tab_group_1 = world.spawn(TabGroup::new(0)).id(); + let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_1))).id(); + let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_1))).id(); + + let tab_group_2 = world.spawn(TabGroup::new(1)).id(); + let tab_entity_3 = world.spawn((TabIndex(0), ChildOf(tab_group_2))).id(); + let tab_entity_4 = world.spawn((TabIndex(1), ChildOf(tab_group_2))).id(); + + let mut system_state: SystemState = SystemState::new(world); + let tab_navigation = system_state.get(world); + assert_eq!(tab_navigation.tabgroup_query.iter().count(), 2); + assert_eq!(tab_navigation.tabindex_query.iter().count(), 4); + + let next_entity = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next); + assert_eq!(next_entity, Ok(tab_entity_2)); + + let prev_entity = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous); + assert_eq!(prev_entity, Ok(tab_entity_1)); + + let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First); + assert_eq!(first_entity, Ok(tab_entity_1)); + + let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last); + assert_eq!(last_entity, Ok(tab_entity_4)); + + let next_from_end_of_group_entity = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Next); + assert_eq!(next_from_end_of_group_entity, Ok(tab_entity_3)); + + let prev_entity_from_start_of_group = + tab_navigation.navigate(&InputFocus::from_entity(tab_entity_3), NavAction::Previous); + assert_eq!(prev_entity_from_start_of_group, Ok(tab_entity_2)); + } } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 28d234f2b4..4692fe9d15 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_internal" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "An internal Bevy crate used to facilitate optional dynamic linking via the 'dynamic_linking' feature" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["game", "engine", "gamedev", "graphics", "bevy"] @@ -279,9 +279,6 @@ custom_cursor = ["bevy_winit/custom_cursor"] # Experimental support for nodes that are ignored for UI layouting ghost_nodes = ["bevy_ui/ghost_nodes"] -# Use the configurable global error handler as the default error handler. -configurable_error_handler = ["bevy_ecs/configurable_error_handler"] - # Allows access to the `std` crate. Enabling this feature will prevent compilation # on `no_std` targets, but provides access to certain additional features on # supported platforms. @@ -299,7 +296,6 @@ std = [ "bevy_state?/std", "bevy_time/std", "bevy_transform/std", - "bevy_utils/std", "bevy_tasks/std", "bevy_window?/std", ] @@ -317,7 +313,6 @@ critical-section = [ "bevy_reflect/critical-section", "bevy_state?/critical-section", "bevy_time/critical-section", - "bevy_utils/critical-section", "bevy_tasks/critical-section", ] @@ -349,82 +344,86 @@ web = [ "bevy_tasks/web", ] +hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] + +debug = ["bevy_utils/debug"] + [dependencies] # bevy (no_std) -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev", default-features = false } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false, features = [ +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev", default-features = false } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false, features = [ +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", "nostd-libm", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "smallvec", ] } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev", default-features = false, features = [ +bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev", default-features = false, features = [ +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev", default-features = false, features = [ "bevy-support", "bevy_reflect", ] } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ - "alloc", -] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false } # bevy (std required) -bevy_log = { path = "../bevy_log", version = "0.16.0-dev", optional = true } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev", optional = true } # bevy (optional) -bevy_a11y = { path = "../bevy_a11y", optional = true, version = "0.16.0-dev", features = [ +bevy_a11y = { path = "../bevy_a11y", optional = true, version = "0.17.0-dev", features = [ "bevy_reflect", ] } -bevy_animation = { path = "../bevy_animation", optional = true, version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", optional = true, version = "0.16.0-dev" } -bevy_audio = { path = "../bevy_audio", optional = true, version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_animation = { path = "../bevy_animation", optional = true, version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", optional = true, version = "0.17.0-dev" } +bevy_audio = { path = "../bevy_audio", optional = true, version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", optional = true, version = "0.17.0-dev", default-features = false, features = [ "alloc", "bevy_reflect", ] } -bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } -bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } -bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } -bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } -bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.16.0-dev", default-features = false } -bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", optional = true, version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.17.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.17.0-dev" } +bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.17.0-dev" } +bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17.0-dev" } +bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false } +bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-dev" } -bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" } -bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" } -bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", optional = true, version = "0.17.0-dev" } +bevy_remote = { path = "../bevy_remote", optional = true, version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", optional = true, version = "0.17.0-dev" } +bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.0-dev" } +bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_app", "bevy_reflect", ] } -bevy_text = { path = "../bevy_text", optional = true, version = "0.16.0-dev" } -bevy_ui = { path = "../bevy_ui", optional = true, version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", optional = true, version = "0.16.0-dev", default-features = false, features = [ +bevy_text = { path = "../bevy_text", optional = true, version = "0.17.0-dev" } +bevy_ui = { path = "../bevy_ui", optional = true, version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_reflect", ] } -bevy_winit = { path = "../bevy_winit", optional = true, version = "0.16.0-dev", default-features = false } +bevy_winit = { path = "../bevy_winit", optional = true, version = "0.17.0-dev", default-features = false } [lints] workspace = true diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index db1152a362..93ba3cb889 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -19,7 +19,7 @@ plugin_group! { #[cfg(feature = "bevy_window")] bevy_a11y:::AccessibilityPlugin, #[cfg(feature = "std")] - #[custom(cfg(any(unix, windows)))] + #[custom(cfg(any(all(unix, not(target_os = "horizon")), windows)))] bevy_app:::TerminalCtrlCHandlerPlugin, #[cfg(feature = "bevy_asset")] bevy_asset:::AssetPlugin, @@ -66,6 +66,8 @@ plugin_group! { bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[cfg(feature = "hotpatching")] + bevy_app::hotpatch:::HotPatchPlugin, #[plugin_group] #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 07dd936ab1..b9934088f1 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -29,6 +29,8 @@ pub use bevy_audio as audio; pub use bevy_color as color; #[cfg(feature = "bevy_core_pipeline")] pub use bevy_core_pipeline as core_pipeline; +#[cfg(feature = "bevy_core_widgets")] +pub use bevy_core_widgets as core_widgets; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; @@ -60,6 +62,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_solari")] +pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; #[cfg(feature = "bevy_state")] diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index cc7c53e676..3dcfa27794 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_log" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides logging for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -14,9 +14,10 @@ trace_tracy_memory = ["dep:tracy-client"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } # other tracing-subscriber = { version = "0.3.1", features = [ @@ -39,12 +40,12 @@ android_log-sys = "0.3.0" [target.'cfg(target_arch = "wasm32")'.dependencies] tracing-wasm = "0.2.1" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } [target.'cfg(target_os = "ios")'.dependencies] -tracing-oslog = "0.2" +tracing-oslog = "0.3" [lints] workspace = true diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index 6562fef7fa..7a80a21cc3 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -1,10 +1,10 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] -//! This crate provides logging functions and configuration for [Bevy](https://bevyengine.org) +//! This crate provides logging functions and configuration for [Bevy](https://bevy.org) //! apps, and automatically configures platform specific log handlers (i.e. Wasm or Android). //! //! The macros provided for logging are reexported from [`tracing`](https://docs.rs/tracing), @@ -64,7 +64,7 @@ use tracing_subscriber::{ #[cfg(feature = "tracing-chrome")] use { bevy_ecs::resource::Resource, - bevy_utils::synccell::SyncCell, + bevy_platform::cell::SyncCell, tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}, }; diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 36be752349..b998ae4fd4 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_macro_utils" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A collection of utils for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] diff --git a/crates/bevy_macro_utils/src/bevy_manifest.rs b/crates/bevy_macro_utils/src/bevy_manifest.rs index 8d32781069..b6df0e0e0f 100644 --- a/crates/bevy_macro_utils/src/bevy_manifest.rs +++ b/crates/bevy_macro_utils/src/bevy_manifest.rs @@ -95,7 +95,7 @@ impl BevyManifest { return None; }; - let mut path = Self::parse_str::(&format!("::{}", package)); + let mut path = Self::parse_str::(&format!("::{package}")); if let Some(module) = name.strip_prefix("bevy_") { path.segments.push(Self::parse_str(module)); } diff --git a/crates/bevy_macro_utils/src/label.rs b/crates/bevy_macro_utils/src/label.rs index 1fc540c9c4..7669f85f1a 100644 --- a/crates/bevy_macro_utils/src/label.rs +++ b/crates/bevy_macro_utils/src/label.rs @@ -58,7 +58,6 @@ pub fn derive_label( input: syn::DeriveInput, trait_name: &str, trait_path: &syn::Path, - dyn_eq_path: &syn::Path, ) -> TokenStream { if let syn::Data::Union(_) = &input.data { let message = format!("Cannot derive {trait_name} for unions."); @@ -89,16 +88,6 @@ pub fn derive_label( fn dyn_clone(&self) -> alloc::boxed::Box { alloc::boxed::Box::new(::core::clone::Clone::clone(self)) } - - fn as_dyn_eq(&self) -> &dyn #dyn_eq_path { - self - } - - fn dyn_hash(&self, mut state: &mut dyn ::core::hash::Hasher) { - let ty_id = ::core::any::TypeId::of::(); - ::core::hash::Hash::hash(&ty_id, &mut state); - ::core::hash::Hash::hash(self, &mut state); - } } }; } diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index aa386101f1..209a92f5de 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! A collection of helper types and functions for working on macros within the Bevy ecosystem. diff --git a/crates/bevy_macro_utils/src/shape.rs b/crates/bevy_macro_utils/src/shape.rs index 2d0124d62a..502738cb9b 100644 --- a/crates/bevy_macro_utils/src/shape.rs +++ b/crates/bevy_macro_utils/src/shape.rs @@ -1,24 +1,29 @@ -use proc_macro::Span; -use syn::{punctuated::Punctuated, token::Comma, Data, DataStruct, Error, Field, Fields}; +use syn::{ + punctuated::Punctuated, spanned::Spanned, token::Comma, Data, DataEnum, DataUnion, Error, + Field, Fields, +}; /// Get the fields of a data structure if that structure is a struct with named fields; /// otherwise, return a compile error that points to the site of the macro invocation. -pub fn get_struct_fields(data: &Data) -> syn::Result<&Punctuated> { +/// +/// `meta` should be the name of the macro calling this function. +pub fn get_struct_fields<'a>( + data: &'a Data, + meta: &str, +) -> syn::Result<&'a Punctuated> { match data { - Data::Struct(DataStruct { - fields: Fields::Named(fields), - .. - }) => Ok(&fields.named), - Data::Struct(DataStruct { - fields: Fields::Unnamed(fields), - .. - }) => Ok(&fields.unnamed), - _ => Err(Error::new( - // This deliberately points to the call site rather than the structure - // body; marking the entire body as the source of the error makes it - // impossible to figure out which `derive` has a problem. - Span::call_site().into(), - "Only structs are supported", + Data::Struct(data_struct) => match &data_struct.fields { + Fields::Named(fields_named) => Ok(&fields_named.named), + Fields::Unnamed(fields_unnamed) => Ok(&fields_unnamed.unnamed), + Fields::Unit => Ok(const { &Punctuated::new() }), + }, + Data::Enum(DataEnum { enum_token, .. }) => Err(Error::new( + enum_token.span(), + format!("#[{meta}] only supports structs, not enums"), + )), + Data::Union(DataUnion { union_token, .. }) => Err(Error::new( + union_token.span(), + format!("#[{meta}] only supports structs, not unions"), )), } } diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 7aae1ec74b..f28e9466ec 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_math" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides math functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -25,7 +25,7 @@ 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.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", ], optional = true } variadics_please = "1.1" diff --git a/crates/bevy_math/src/bounding/raycast2d.rs b/crates/bevy_math/src/bounding/raycast2d.rs index e1def01936..c767c6e3fd 100644 --- a/crates/bevy_math/src/bounding/raycast2d.rs +++ b/crates/bevy_math/src/bounding/raycast2d.rs @@ -78,8 +78,8 @@ impl RayCast2d { pub fn circle_intersection_at(&self, circle: &BoundingCircle) -> Option { let offset = self.ray.origin - circle.center; 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(); + let cross = offset.perp_dot(*self.ray.direction); + let distance_squared = circle.radius().squared() - cross.squared(); if distance_squared < 0. || ops::copysign(projected.squared(), -projected) < -distance_squared { diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 4e127f4026..b249b34618 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,6 +1,6 @@ //! This module contains abstract mathematical traits shared by types used in `bevy_math`. -use crate::{ops, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; +use crate::{ops, DVec2, DVec3, DVec4, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use core::{ fmt::Debug, ops::{Add, Div, Mul, Neg, Sub}, @@ -9,7 +9,7 @@ use variadics_please::all_tuples_enumerated; /// A type that supports the mathematical operations of a real vector space, irrespective of dimension. /// In particular, this means that the implementing type supports: -/// - Scalar multiplication and division on the right by elements of `f32` +/// - Scalar multiplication and division on the right by elements of `Self::Scalar` /// - Negation /// - Addition and subtraction /// - Zero @@ -19,16 +19,16 @@ use variadics_please::all_tuples_enumerated; /// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. /// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. /// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. -/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`. +/// - (Compatibility of multiplication) For all `a, b: Self::Scalar`, `v: Self`, `v * (a * b) == (v * a) * b`. /// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`. -/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`. -/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`. +/// - (Distributivity for vector addition) For all `a: Self::Scalar`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +/// - (Distributivity for scalar addition) For all `a, b: Self::Scalar`, `v: Self`, `v * (a + b) == v * a + v * b`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait VectorSpace: - Mul - + Div + Mul + + Div + Add + Sub + Neg @@ -37,6 +37,9 @@ pub trait VectorSpace: + Clone + Copy { + /// The scalar type of this vector space. + type Scalar: ScalarField; + /// The zero vector, which is the identity of addition for the vector space type. const ZERO: Self; @@ -47,29 +50,99 @@ pub trait VectorSpace: /// Note that the value of `t` is not clamped by this function, so extrapolating outside /// of the interval `[0,1]` is allowed. #[inline] - fn lerp(self, rhs: Self, t: f32) -> Self { - self * (1. - t) + rhs * t + fn lerp(self, rhs: Self, t: Self::Scalar) -> Self { + self * (Self::Scalar::ONE - t) + rhs * t } } impl VectorSpace for Vec4 { + type Scalar = f32; const ZERO: Self = Vec4::ZERO; } impl VectorSpace for Vec3 { + type Scalar = f32; const ZERO: Self = Vec3::ZERO; } impl VectorSpace for Vec3A { + type Scalar = f32; const ZERO: Self = Vec3A::ZERO; } impl VectorSpace for Vec2 { + type Scalar = f32; const ZERO: Self = Vec2::ZERO; } -impl VectorSpace for f32 { +impl VectorSpace for DVec4 { + type Scalar = f64; + const ZERO: Self = DVec4::ZERO; +} + +impl VectorSpace for DVec3 { + type Scalar = f64; + const ZERO: Self = DVec3::ZERO; +} + +impl VectorSpace for DVec2 { + type Scalar = f64; + const ZERO: Self = DVec2::ZERO; +} + +// Every scalar field is a 1-dimensional vector space over itself. +impl VectorSpace for T { + type Scalar = Self; + const ZERO: Self = Self::ZERO; +} + +/// A type that supports the operations of a scalar field. An implementation should support: +/// - Addition and subtraction +/// - Multiplication and division +/// - Negation +/// - Zero (additive identity) +/// - One (multiplicative identity) +/// +/// Within the limitations of floating point arithmetic, all the following are required to hold: +/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`. +/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. +/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. +/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. +/// - (Associativity of multiplication) For all `u, v, w: Self`, `(u * v) * w == u * (v * w)`. +/// - (Commutativity of multiplication) For all `u, v: Self`, `u * v == v * u`. +/// - (Multiplicative identity) For all `v: Self`, `v * Self::ONE == v`. +/// - (Multiplicative inverse) For all `v: Self`, `v / v == v * v.inverse() == Self::ONE`. +/// - (Distributivity over addition) For all `a, b: Self`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +pub trait ScalarField: + Mul + + Div + + Add + + Sub + + Neg + + Default + + Debug + + Clone + + Copy +{ + /// The additive identity. + const ZERO: Self; + /// The multiplicative identity. + const ONE: Self; + + /// The multiplicative inverse of this element. This is equivalent to `1.0 / self`. + fn recip(self) -> Self { + Self::ONE / self + } +} + +impl ScalarField for f32 { const ZERO: Self = 0.0; + const ONE: Self = 1.0; +} + +impl ScalarField for f64 { + const ZERO: Self = 0.0; + const ONE: Self = 1.0; } /// A type consisting of formal sums of elements from `V` and `W`. That is, @@ -84,24 +157,24 @@ impl VectorSpace for f32 { #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] pub struct Sum(pub V, pub W); -impl Mul for Sum +impl Mul for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { type Output = Self; - fn mul(self, rhs: f32) -> Self::Output { + fn mul(self, rhs: F) -> Self::Output { Sum(self.0 * rhs, self.1 * rhs) } } -impl Div for Sum +impl Div for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { type Output = Self; - fn div(self, rhs: f32) -> Self::Output { + fn div(self, rhs: F) -> Self::Output { Sum(self.0 / rhs, self.1 / rhs) } } @@ -149,11 +222,12 @@ where } } -impl VectorSpace for Sum +impl VectorSpace for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { + type Scalar = F; const ZERO: Self = Sum(V::ZERO, W::ZERO); } @@ -162,32 +236,32 @@ where /// relationships hold, within the limitations of floating point arithmetic: /// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`. /// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`. -/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. +/// - (Absolute homogeneity) For all `c: Self::Scalar`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. /// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait NormedVectorSpace: VectorSpace { /// The size of this element. The return value should always be nonnegative. - fn norm(self) -> f32; + fn norm(self) -> Self::Scalar; /// The squared norm of this element. Computing this is often faster than computing /// [`NormedVectorSpace::norm`]. #[inline] - fn norm_squared(self) -> f32 { + fn norm_squared(self) -> Self::Scalar { self.norm() * self.norm() } /// The distance between this element and another, as determined by the norm. #[inline] - fn distance(self, rhs: Self) -> f32 { + fn distance(self, rhs: Self) -> Self::Scalar { (rhs - self).norm() } /// The squared distance between this element and another, as determined by the norm. Note that /// this is often faster to compute in practice than [`NormedVectorSpace::distance`]. #[inline] - fn distance_squared(self, rhs: Self) -> f32 { + fn distance_squared(self, rhs: Self) -> Self::Scalar { (rhs - self).norm_squared() } } @@ -245,10 +319,55 @@ impl NormedVectorSpace for f32 { fn norm(self) -> f32 { ops::abs(self) } +} + +impl NormedVectorSpace for DVec4 { + #[inline] + fn norm(self) -> f64 { + self.length() + } #[inline] - fn norm_squared(self) -> f32 { - self * self + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for DVec3 { + #[inline] + fn norm(self) -> f64 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for DVec2 { + #[inline] + fn norm(self) -> f64 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for f64 { + #[inline] + #[cfg(feature = "std")] + fn norm(self) -> f64 { + f64::abs(self) + } + + #[inline] + #[cfg(all(any(feature = "libm", feature = "nostd-libm"), not(feature = "std")))] + fn norm(self) -> f64 { + libm::fabs(self) } } @@ -353,7 +472,7 @@ pub trait StableInterpolate: Clone { // VectorSpace type, but the "natural from the semantics" part is less clear in general. impl StableInterpolate for V where - V: NormedVectorSpace, + V: NormedVectorSpace, { #[inline] fn interpolate_stable(&self, other: &Self, t: f32) -> Self { @@ -462,10 +581,13 @@ impl HasTangent for V { type Tangent = V; } -impl HasTangent for (M, N) +impl HasTangent for (M, N) where - M: HasTangent, - N: HasTangent, + F: ScalarField, + U: VectorSpace, + V: VectorSpace, + M: HasTangent, + N: HasTangent, { type Tangent = Sum; } diff --git a/crates/bevy_math/src/cubic_splines/curve_impls.rs b/crates/bevy_math/src/cubic_splines/curve_impls.rs index 85fd9fb6ad..c21763db4e 100644 --- a/crates/bevy_math/src/cubic_splines/curve_impls.rs +++ b/crates/bevy_math/src/cubic_splines/curve_impls.rs @@ -10,7 +10,7 @@ use super::{CubicCurve, RationalCurve}; // -- CubicSegment -impl Curve

for CubicSegment

{ +impl> Curve

for CubicSegment

{ #[inline] fn domain(&self) -> Interval { Interval::UNIT @@ -22,7 +22,7 @@ impl Curve

for CubicSegment

{ } } -impl SampleDerivative

for CubicSegment

{ +impl> SampleDerivative

for CubicSegment

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -32,7 +32,7 @@ impl SampleDerivative

for CubicSegment

{ } } -impl SampleTwoDerivatives

for CubicSegment

{ +impl> SampleTwoDerivatives

for CubicSegment

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -46,7 +46,7 @@ impl SampleTwoDerivatives

for CubicSegment

{ // -- CubicCurve #[cfg(feature = "alloc")] -impl Curve

for CubicCurve

{ +impl> Curve

for CubicCurve

{ #[inline] fn domain(&self) -> Interval { // The non-emptiness invariant guarantees that this succeeds. @@ -61,7 +61,7 @@ impl Curve

for CubicCurve

{ } #[cfg(feature = "alloc")] -impl SampleDerivative

for CubicCurve

{ +impl> SampleDerivative

for CubicCurve

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -72,7 +72,7 @@ impl SampleDerivative

for CubicCurve

{ } #[cfg(feature = "alloc")] -impl SampleTwoDerivatives

for CubicCurve

{ +impl> SampleTwoDerivatives

for CubicCurve

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -85,7 +85,7 @@ impl SampleTwoDerivatives

for CubicCurve

{ // -- RationalSegment -impl Curve

for RationalSegment

{ +impl> Curve

for RationalSegment

{ #[inline] fn domain(&self) -> Interval { Interval::UNIT @@ -97,7 +97,7 @@ impl Curve

for RationalSegment

{ } } -impl SampleDerivative

for RationalSegment

{ +impl> SampleDerivative

for RationalSegment

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -107,7 +107,7 @@ impl SampleDerivative

for RationalSegment

{ } } -impl SampleTwoDerivatives

for RationalSegment

{ +impl> SampleTwoDerivatives

for RationalSegment

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -121,7 +121,7 @@ impl SampleTwoDerivatives

for RationalSegment

{ // -- RationalCurve #[cfg(feature = "alloc")] -impl Curve

for RationalCurve

{ +impl> Curve

for RationalCurve

{ #[inline] fn domain(&self) -> Interval { // The non-emptiness invariant guarantees the success of this. @@ -136,7 +136,7 @@ impl Curve

for RationalCurve

{ } #[cfg(feature = "alloc")] -impl SampleDerivative

for RationalCurve

{ +impl> SampleDerivative

for RationalCurve

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -147,7 +147,7 @@ impl SampleDerivative

for RationalCurve

{ } #[cfg(feature = "alloc")] -impl SampleTwoDerivatives

for RationalCurve

{ +impl> SampleTwoDerivatives

for RationalCurve

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index 6f60de774a..3ea99a60b0 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -68,7 +68,7 @@ impl CubicBezier

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

for CubicBezier

{ +impl> CubicGenerator

for CubicBezier

{ type Error = CubicBezierError; #[inline] @@ -176,7 +176,7 @@ impl CubicHermite

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

for CubicHermite

{ +impl> CubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; #[inline] @@ -202,7 +202,7 @@ impl CubicGenerator

for CubicHermite

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

for CubicHermite

{ +impl> CyclicCubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; #[inline] @@ -313,7 +313,7 @@ impl CubicCardinalSpline

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

for CubicCardinalSpline

{ +impl> CubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; #[inline] @@ -351,7 +351,7 @@ impl CubicGenerator

for CubicCardinalSpline

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

for CubicCardinalSpline

{ +impl> CyclicCubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; #[inline] @@ -471,7 +471,7 @@ impl CubicBSpline

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

for CubicBSpline

{ +impl> CubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; #[inline] @@ -494,7 +494,7 @@ impl CubicGenerator

for CubicBSpline

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

for CubicBSpline

{ +impl> CyclicCubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; #[inline] @@ -620,7 +620,7 @@ pub struct CubicNurbs { } #[cfg(feature = "alloc")] -impl CubicNurbs

{ +impl> CubicNurbs

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

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

for CubicNurbs

{ +impl> RationalGenerator

for CubicNurbs

{ type Error = InsufficientDataError; #[inline] @@ -962,7 +962,7 @@ pub struct CubicSegment { pub coeff: [P; 4], } -impl CubicSegment

{ +impl> CubicSegment

{ /// Instantaneous position of a point at parametric value `t`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1184,7 +1184,7 @@ pub struct CubicCurve { } #[cfg(feature = "alloc")] -impl CubicCurve

{ +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. pub fn from_segments(segments: impl IntoIterator>) -> Option { @@ -1347,7 +1347,8 @@ pub struct RationalSegment { /// The width of the domain of this segment. pub knot_span: f32, } -impl RationalSegment

{ + +impl> RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, 1]`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1484,7 +1485,7 @@ pub struct RationalCurve { } #[cfg(feature = "alloc")] -impl RationalCurve

{ +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. pub fn from_segments(segments: impl IntoIterator>) -> Option { @@ -1788,9 +1789,7 @@ mod tests { for (i, (a, b)) in cubic_curve.iter().zip(rational_curve.iter()).enumerate() { assert!( a.distance(*b) < EPSILON, - "Mismatch at {name} value {i}. CubicCurve: {} Converted RationalCurve: {}", - a, - b + "Mismatch at {name} value {i}. CubicCurve: {a} Converted RationalCurve: {b}", ); } } diff --git a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs index a499526b78..9e3686b5aa 100644 --- a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs +++ b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs @@ -208,10 +208,12 @@ where // -- ZipCurve -impl SampleDerivative<(S, T)> for ZipCurve +impl SampleDerivative<(S, T)> for ZipCurve where - S: HasTangent, - T: HasTangent, + U: VectorSpace, + V: VectorSpace, + S: HasTangent, + T: HasTangent, C: SampleDerivative, D: SampleDerivative, { @@ -225,10 +227,12 @@ where } } -impl SampleTwoDerivatives<(S, T)> for ZipCurve +impl SampleTwoDerivatives<(S, T)> for ZipCurve where - S: HasTangent, - T: HasTangent, + U: VectorSpace, + V: VectorSpace, + S: HasTangent, + T: HasTangent, C: SampleTwoDerivatives, D: SampleTwoDerivatives, { @@ -248,9 +252,10 @@ where // -- GraphCurve -impl SampleDerivative<(f32, T)> for GraphCurve +impl SampleDerivative<(f32, T)> for GraphCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(f32, T)> { @@ -262,9 +267,10 @@ where } } -impl SampleTwoDerivatives<(f32, T)> for GraphCurve +impl SampleTwoDerivatives<(f32, T)> for GraphCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, { fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(f32, T)> { @@ -321,9 +327,10 @@ where // -- CurveReparamCurve -impl SampleDerivative for CurveReparamCurve +impl SampleDerivative for CurveReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, D: SampleDerivative, { @@ -349,9 +356,10 @@ where } } -impl SampleTwoDerivatives for CurveReparamCurve +impl SampleTwoDerivatives for CurveReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, D: SampleTwoDerivatives, { @@ -386,9 +394,10 @@ where // -- LinearReparamCurve -impl SampleDerivative for LinearReparamCurve +impl SampleDerivative for LinearReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { @@ -413,9 +422,10 @@ where } } -impl SampleTwoDerivatives for LinearReparamCurve +impl SampleTwoDerivatives for LinearReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, { fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index c0b452e001..91908ee80b 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -32,7 +32,7 @@ pub trait Ease: Sized { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve; } -impl Ease for V { +impl> Ease for V { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) } diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index 45138f20e2..f5ecf75c08 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -1,6 +1,6 @@ use crate::{ primitives::{Primitive2d, Primitive3d}, - Quat, Rot2, Vec2, Vec3, Vec3A, + Quat, Rot2, Vec2, Vec3, Vec3A, Vec4, }; use core::f32::consts::FRAC_1_SQRT_2; @@ -866,6 +866,195 @@ impl approx::UlpsEq for Dir3A { } } +/// A normalized vector pointing in a direction in 4D space +#[derive(Clone, Copy, Debug, PartialEq, Into)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +#[doc(alias = "Direction4d")] +pub struct Dir4(Vec4); + +impl Dir4 { + /// A unit vector pointing along the positive X axis + pub const X: Self = Self(Vec4::X); + /// A unit vector pointing along the positive Y axis. + pub const Y: Self = Self(Vec4::Y); + /// A unit vector pointing along the positive Z axis. + pub const Z: Self = Self(Vec4::Z); + /// A unit vector pointing along the positive W axis. + pub const W: Self = Self(Vec4::W); + /// A unit vector pointing along the negative X axis. + pub const NEG_X: Self = Self(Vec4::NEG_X); + /// A unit vector pointing along the negative Y axis. + pub const NEG_Y: Self = Self(Vec4::NEG_Y); + /// A unit vector pointing along the negative Z axis. + pub const NEG_Z: Self = Self(Vec4::NEG_Z); + /// A unit vector pointing along the negative W axis. + pub const NEG_W: Self = Self(Vec4::NEG_W); + /// The directional axes. + pub const AXES: [Self; 4] = [Self::X, Self::Y, Self::Z, Self::W]; + + /// Create a direction from a finite, nonzero [`Vec4`], normalizing it. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the given vector is zero (or very close to zero), infinite, or `NaN`. + pub fn new(value: Vec4) -> Result { + Self::new_and_length(value).map(|(dir, _)| dir) + } + + /// Create a [`Dir4`] from a [`Vec4`] that is already normalized. + /// + /// # Warning + /// + /// `value` must be normalized, i.e its length must be `1.0`. + pub fn new_unchecked(value: Vec4) -> Self { + #[cfg(debug_assertions)] + assert_is_normalized( + "The vector given to `Dir4::new_unchecked` is not normalized.", + value.length_squared(), + ); + Self(value) + } + + /// Create a direction from a finite, nonzero [`Vec4`], normalizing it and + /// also returning its original length. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the given vector is zero (or very close to zero), infinite, or `NaN`. + pub fn new_and_length(value: Vec4) -> Result<(Self, f32), InvalidDirectionError> { + let length = value.length(); + let direction = (length.is_finite() && length > 0.0).then_some(value / length); + + direction + .map(|dir| (Self(dir), length)) + .ok_or(InvalidDirectionError::from_length(length)) + } + + /// Create a direction from its `x`, `y`, `z`, and `w` components. + /// + /// Returns [`Err(InvalidDirectionError)`](InvalidDirectionError) if the length + /// of the vector formed by the components is zero (or very close to zero), infinite, or `NaN`. + pub fn from_xyzw(x: f32, y: f32, z: f32, w: f32) -> Result { + Self::new(Vec4::new(x, y, z, w)) + } + + /// Create a direction from its `x`, `y`, `z`, and `w` components, assuming the resulting vector is normalized. + /// + /// # Warning + /// + /// The vector produced from `x`, `y`, `z`, and `w` must be normalized, i.e its length must be `1.0`. + pub fn from_xyzw_unchecked(x: f32, y: f32, z: f32, w: f32) -> Self { + Self::new_unchecked(Vec4::new(x, y, z, w)) + } + + /// Returns the inner [`Vec4`] + pub const fn as_vec4(&self) -> Vec4 { + self.0 + } + + /// Returns `self` after an approximate normalization, assuming the value is already nearly normalized. + /// Useful for preventing numerical error accumulation. + #[inline] + pub fn fast_renormalize(self) -> Self { + // We numerically approximate the inverse square root by a Taylor series around 1 + // As we expect the error (x := length_squared - 1) to be small + // inverse_sqrt(length_squared) = (1 + x)^(-1/2) = 1 - 1/2 x + O(x²) + // inverse_sqrt(length_squared) ≈ 1 - 1/2 (length_squared - 1) = 1/2 (3 - length_squared) + + // Iterative calls to this method quickly converge to a normalized value, + // so long as the denormalization is not large ~ O(1/10). + // One iteration can be described as: + // l_sq <- l_sq * (1 - 1/2 (l_sq - 1))²; + // Rewriting in terms of the error x: + // 1 + x <- (1 + x) * (1 - 1/2 x)² + // 1 + x <- (1 + x) * (1 - x + 1/4 x²) + // 1 + x <- 1 - x + 1/4 x² + x - x² + 1/4 x³ + // x <- -1/4 x² (3 - x) + // If the error is small, say in a range of (-1/2, 1/2), then: + // |-1/4 x² (3 - x)| <= (3/4 + 1/4 * |x|) * x² <= (3/4 + 1/4 * 1/2) * x² < x² < 1/2 x + // Therefore the sequence of iterates converges to 0 error as a second order method. + + let length_squared = self.0.length_squared(); + Self(self * (0.5 * (3.0 - length_squared))) + } +} + +impl TryFrom for Dir4 { + type Error = InvalidDirectionError; + + fn try_from(value: Vec4) -> Result { + Self::new(value) + } +} + +impl core::ops::Deref for Dir4 { + type Target = Vec4; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::Neg for Dir4 { + type Output = Self; + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + +impl core::ops::Mul for Dir4 { + type Output = Vec4; + fn mul(self, rhs: f32) -> Self::Output { + self.0 * rhs + } +} + +impl core::ops::Mul for f32 { + type Output = Vec4; + fn mul(self, rhs: Dir4) -> Self::Output { + self * rhs.0 + } +} + +#[cfg(feature = "approx")] +impl approx::AbsDiffEq for Dir4 { + type Epsilon = f32; + fn default_epsilon() -> f32 { + f32::EPSILON + } + fn abs_diff_eq(&self, other: &Self, epsilon: f32) -> bool { + self.as_ref().abs_diff_eq(other.as_ref(), epsilon) + } +} + +#[cfg(feature = "approx")] +impl approx::RelativeEq for Dir4 { + fn default_max_relative() -> f32 { + f32::EPSILON + } + fn relative_eq(&self, other: &Self, epsilon: f32, max_relative: f32) -> bool { + self.as_ref() + .relative_eq(other.as_ref(), epsilon, max_relative) + } +} + +#[cfg(feature = "approx")] +impl approx::UlpsEq for Dir4 { + fn default_max_ulps() -> u32 { + 4 + } + + fn ulps_eq(&self, other: &Self, epsilon: f32, max_ulps: u32) -> bool { + self.as_ref().ulps_eq(other.as_ref(), epsilon, max_ulps) + } +} + #[cfg(test)] #[cfg(feature = "approx")] mod tests { @@ -1090,4 +1279,48 @@ mod tests { ); assert!(dir_b.is_normalized(), "Renormalisation did not work."); } + + #[test] + fn dir4_creation() { + assert_eq!(Dir4::new(Vec4::X * 12.5), Ok(Dir4::X)); + assert_eq!( + Dir4::new(Vec4::new(0.0, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Zero) + ); + assert_eq!( + Dir4::new(Vec4::new(f32::INFINITY, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Dir4::new(Vec4::new(f32::NEG_INFINITY, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::Infinite) + ); + assert_eq!( + Dir4::new(Vec4::new(f32::NAN, 0.0, 0.0, 0.0)), + Err(InvalidDirectionError::NaN) + ); + assert_eq!(Dir4::new_and_length(Vec4::X * 6.5), Ok((Dir4::X, 6.5))); + } + + #[test] + fn dir4_renorm() { + // Evil denormalized matrix + let mat4 = bevy_math::Mat4::from_quat(Quat::from_euler(glam::EulerRot::XYZ, 1.0, 2.0, 3.0)) + * (1.0 + 1e-5); + let mut dir_a = Dir4::from_xyzw(1., 1., 0., 0.).unwrap(); + let mut dir_b = Dir4::from_xyzw(1., 1., 0., 0.).unwrap(); + // We test that renormalizing an already normalized dir doesn't do anything + assert_relative_eq!(dir_b, dir_b.fast_renormalize(), epsilon = 0.000001); + for _ in 0..50 { + dir_a = Dir4(mat4 * *dir_a); + dir_b = Dir4(mat4 * *dir_b); + dir_b = dir_b.fast_renormalize(); + } + // `dir_a` should've gotten denormalized, meanwhile `dir_b` should stay normalized. + assert!( + !dir_a.is_normalized(), + "Denormalization doesn't work, test is faulty" + ); + assert!(dir_b.is_normalized(), "Renormalisation did not work."); + } } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 20d458db72..070483e777 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -9,8 +9,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index d666849840..9cb379706c 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -35,6 +35,7 @@ pub struct Circle { /// The radius of the circle pub radius: f32, } + impl Primitive2d for Circle {} impl Default for Circle { @@ -124,6 +125,7 @@ pub struct Arc2d { /// Half the angle defining the arc pub half_angle: f32, } + impl Primitive2d for Arc2d {} impl Default for Arc2d { @@ -290,6 +292,7 @@ pub struct CircularSector { #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } + impl Primitive2d for CircularSector {} impl Default for CircularSector { @@ -433,6 +436,7 @@ pub struct CircularSegment { #[cfg_attr(all(feature = "serialize", feature = "alloc"), serde(flatten))] pub arc: Arc2d, } + impl Primitive2d for CircularSegment {} impl Default for CircularSegment { @@ -453,6 +457,7 @@ impl Measured2d for CircularSegment { self.chord_length() + self.arc_length() } } + impl CircularSegment { /// Create a new [`CircularSegment`] from a `radius`, and an `angle` #[inline(always)] @@ -788,6 +793,7 @@ pub struct Ellipse { /// This corresponds to the two perpendicular radii defining the ellipse. pub half_size: Vec2, } + impl Primitive2d for Ellipse {} impl Default for Ellipse { @@ -939,6 +945,7 @@ pub struct Annulus { /// The outer circle of the annulus pub outer_circle: Circle, } + impl Primitive2d for Annulus {} impl Default for Annulus { @@ -1036,6 +1043,7 @@ pub struct Rhombus { /// Size of the horizontal and vertical diagonals of the rhombus pub half_diagonals: Vec2, } + impl Primitive2d for Rhombus {} impl Default for Rhombus { @@ -1171,6 +1179,7 @@ pub struct Plane2d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir2, } + impl Primitive2d for Plane2d {} impl Default for Plane2d { @@ -1213,6 +1222,7 @@ pub struct Line2d { /// and its opposite direction pub direction: Dir2, } + impl Primitive2d for Line2d {} /// A line segment defined by two endpoints in 2D space. @@ -1232,6 +1242,7 @@ pub struct Segment2d { /// The endpoints of the line segment. pub vertices: [Vec2; 2], } + impl Primitive2d for Segment2d {} impl Segment2d { @@ -1504,6 +1515,7 @@ pub struct Polyline2d { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec2; N], } + impl Primitive2d for Polyline2d {} impl FromIterator for Polyline2d { @@ -1573,6 +1585,7 @@ pub struct Triangle2d { /// The vertices of the triangle pub vertices: [Vec2; 3], } + impl Primitive2d for Triangle2d {} impl Default for Triangle2d { @@ -1745,6 +1758,7 @@ pub struct Rectangle { /// Half of the width and height of the rectangle pub half_size: Vec2, } + impl Primitive2d for Rectangle {} impl Default for Rectangle { @@ -1838,6 +1852,7 @@ pub struct Polygon { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec2; N], } + impl Primitive2d for Polygon {} impl FromIterator for Polygon { @@ -1892,6 +1907,7 @@ pub struct ConvexPolygon { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] vertices: [Vec2; N], } + impl Primitive2d for ConvexPolygon {} /// An error that happens when creating a [`ConvexPolygon`]. @@ -2013,6 +2029,7 @@ pub struct RegularPolygon { /// The number of sides pub sides: u32, } + impl Primitive2d for RegularPolygon {} impl Default for RegularPolygon { @@ -2160,6 +2177,7 @@ pub struct Capsule2d { /// Half the height of the capsule, excluding the semicircles pub half_length: f32, } + impl Primitive2d for Capsule2d {} impl Default for Capsule2d { diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index ea5ccd6e2d..86aa6c5bdf 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -31,6 +31,7 @@ pub struct Sphere { /// The radius of the sphere pub radius: f32, } + impl Primitive3d for Sphere {} impl Default for Sphere { @@ -105,6 +106,7 @@ pub struct Plane3d { /// Half of the width and height of the plane pub half_size: Vec2, } + impl Primitive3d for Plane3d {} impl Default for Plane3d { @@ -175,6 +177,7 @@ pub struct InfinitePlane3d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir3, } + impl Primitive3d for InfinitePlane3d {} impl Default for InfinitePlane3d { @@ -351,6 +354,7 @@ pub struct Line3d { /// The direction of the line pub direction: Dir3, } + impl Primitive3d for Line3d {} /// A line segment defined by two endpoints in 3D space. @@ -370,6 +374,7 @@ pub struct Segment3d { /// The endpoints of the line segment. pub vertices: [Vec3; 2], } + impl Primitive3d for Segment3d {} impl Segment3d { @@ -578,6 +583,7 @@ pub struct Polyline3d { #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] pub vertices: [Vec3; N], } + impl Primitive3d for Polyline3d {} impl FromIterator for Polyline3d { @@ -648,6 +654,7 @@ pub struct Cuboid { /// Half of the width, height and depth of the cuboid pub half_size: Vec3, } + impl Primitive3d for Cuboid {} impl Default for Cuboid { @@ -742,6 +749,7 @@ pub struct Cylinder { /// The half height of the cylinder pub half_height: f32, } + impl Primitive3d for Cylinder {} impl Default for Cylinder { @@ -820,6 +828,7 @@ pub struct Capsule3d { /// Half the height of the capsule, excluding the hemispheres pub half_length: f32, } + impl Primitive3d for Capsule3d {} impl Default for Capsule3d { @@ -890,6 +899,7 @@ pub struct Cone { /// The height of the cone pub height: f32, } + impl Primitive3d for Cone {} impl Default for Cone { @@ -974,6 +984,7 @@ pub struct ConicalFrustum { /// The height of the frustum pub height: f32, } + impl Primitive3d for ConicalFrustum {} impl Default for ConicalFrustum { @@ -1030,6 +1041,7 @@ pub struct Torus { #[doc(alias = "radius_of_revolution")] pub major_radius: f32, } + impl Primitive3d for Torus {} impl Default for Torus { @@ -1326,6 +1338,7 @@ pub struct Tetrahedron { /// The vertices of the tetrahedron. pub vertices: [Vec3; 4], } + impl Primitive3d for Tetrahedron {} impl Default for Tetrahedron { @@ -1433,6 +1446,7 @@ pub struct Extrusion { /// Half of the depth of the extrusion pub half_depth: f32, } + impl Primitive3d for Extrusion {} impl Extrusion { diff --git a/crates/bevy_math/src/primitives/polygon.rs b/crates/bevy_math/src/primitives/polygon.rs index 20d35b552c..9aa261b297 100644 --- a/crates/bevy_math/src/primitives/polygon.rs +++ b/crates/bevy_math/src/primitives/polygon.rs @@ -34,6 +34,7 @@ struct SweepLineEvent { /// Type of the vertex (left or right) endpoint: Endpoint, } + impl SweepLineEvent { #[cfg_attr( not(feature = "alloc"), @@ -46,17 +47,21 @@ impl SweepLineEvent { } } } + impl PartialEq for SweepLineEvent { fn eq(&self, other: &Self) -> bool { self.position() == other.position() } } + impl Eq for SweepLineEvent {} + impl PartialOrd for SweepLineEvent { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } + impl Ord for SweepLineEvent { fn cmp(&self, other: &Self) -> Ordering { xy_order(self.position(), other.position()) @@ -129,11 +134,13 @@ struct Segment { left: Vec2, right: Vec2, } + impl PartialEq for Segment { fn eq(&self, other: &Self) -> bool { self.edge_index == other.edge_index } } + impl Eq for Segment {} impl PartialOrd for Segment { @@ -141,6 +148,7 @@ impl PartialOrd for Segment { Some(self.cmp(other)) } } + impl Ord for Segment { fn cmp(&self, other: &Self) -> Ordering { self.left diff --git a/crates/bevy_math/src/primitives/serde.rs b/crates/bevy_math/src/primitives/serde.rs index 7db6be9700..a1b678132e 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_fmt(format_args!("an array of length {}", N)) + formatter.write_fmt(format_args!("an array of length {N}")) } #[inline] diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 3be0ead1da..c17bc6fa76 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -40,11 +40,12 @@ use core::f32::consts::{PI, TAU}; -use crate::{ops, primitives::*, NormedVectorSpace, Vec2, Vec3}; +use crate::{ops, primitives::*, NormedVectorSpace, ScalarField, Vec2, Vec3}; use rand::{ distributions::{Distribution, WeightedIndex}, Rng, }; +use rand_distr::uniform::SampleUniform; /// Exposes methods to uniformly sample a variety of primitive shapes. pub trait ShapeSample { @@ -281,22 +282,24 @@ impl ShapeSample for Cuboid { } /// Interior sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_interior( - vertices: [P; 3], - rng: &mut R, -) -> P { +fn sample_triangle_interior(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd, + R: Rng + ?Sized, +{ let [a, b, c] = vertices; let ab = b - a; let ac = c - a; // Generate random points on a parallelepiped and reflect so that // we can use the points that lie outside the triangle - let u = rng.gen_range(0.0..=1.0); - let v = rng.gen_range(0.0..=1.0); + let u = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); + let v = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); - if u + v > 1. { - let u1 = 1. - v; - let v1 = 1. - u; + if u + v > P::Scalar::ONE { + let u1 = P::Scalar::ONE - v; + let v1 = P::Scalar::ONE - u; a + (ab * u1 + ac * v1) } else { a + (ab * u + ac * v) @@ -304,16 +307,18 @@ fn sample_triangle_interior( } /// Boundary sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_boundary( - vertices: [P; 3], - rng: &mut R, -) -> P { +fn sample_triangle_boundary(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd + for<'a> ::core::ops::AddAssign<&'a P::Scalar>, + R: Rng + ?Sized, +{ let [a, b, c] = vertices; let ab = b - a; let ac = c - a; let bc = c - b; - let t = rng.gen_range(0.0..=1.0); + let t = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { match dist.sample(rng) { diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index 2ccb65cdb4..c65c648cfd 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -1,25 +1,25 @@ [package] name = "bevy_mesh" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides mesh types for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] # bevy -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_mesh/src/index.rs b/crates/bevy_mesh/src/index.rs index d2497e2c50..ca84d63bfb 100644 --- a/crates/bevy_mesh/src/index.rs +++ b/crates/bevy_mesh/src/index.rs @@ -163,6 +163,7 @@ impl Iterator for IndicesIter<'_> { } impl<'a> ExactSizeIterator for IndicesIter<'a> {} + impl<'a> FusedIterator for IndicesIter<'a> {} impl From<&Indices> for IndexFormat { diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index e4868dbf69..893c84ecc5 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -119,6 +119,21 @@ pub struct Mesh { morph_targets: Option>, morph_target_names: Option>, pub asset_usage: RenderAssetUsages, + /// Whether or not to build a BLAS for use with `bevy_solari` raytracing. + /// + /// Note that this is _not_ whether the mesh is _compatible_ with `bevy_solari` raytracing. + /// This field just controls whether or not a BLAS gets built for this mesh, assuming that + /// the mesh is compatible. + /// + /// The use case for this field is using lower-resolution proxy meshes for raytracing (to save on BLAS memory usage), + /// while using higher-resolution meshes for raster. You can set this field to true for the lower-resolution proxy mesh, + /// and to false for the high-resolution raster mesh. + /// + /// Alternatively, you can use the same mesh for both raster and raytracing, with this field set to true. + /// + /// Does nothing if not used with `bevy_solari`, or if the mesh is not compatible + /// with `bevy_solari` (see `bevy_solari`'s docs). + pub enable_raytracing: bool, } impl Mesh { @@ -203,6 +218,7 @@ impl Mesh { morph_targets: None, morph_target_names: None, asset_usage, + enable_raytracing: true, } } diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index a8ff3be037..fdeeeacc31 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -117,6 +117,7 @@ pub struct MorphWeights { /// The first mesh primitive assigned to these weights first_mesh: Option>, } + impl MorphWeights { pub fn new( weights: Vec, @@ -160,6 +161,7 @@ impl MorphWeights { pub struct MeshMorphWeights { weights: Vec, } + impl MeshMorphWeights { pub fn new(weights: Vec) -> Result { if weights.len() > MAX_MORPH_WEIGHTS { @@ -198,6 +200,7 @@ pub struct MorphAttributes { /// animated, as the `w` component is the sign and cannot be animated. pub tangent: Vec3, } + impl From<[Vec3; 3]> for MorphAttributes { fn from([position, normal, tangent]: [Vec3; 3]) -> Self { MorphAttributes { @@ -207,6 +210,7 @@ impl From<[Vec3; 3]> for MorphAttributes { } } } + impl MorphAttributes { /// How many components `MorphAttributes` has. /// diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index fbca931fe2..82c2d86553 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mikktspace" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" authors = [ "Benjamin Wasty ", @@ -9,7 +9,7 @@ authors = [ ] description = "Mikkelsen tangent space algorithm" documentation = "https://docs.rs/bevy" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index ee5f149a8c..12efbf5d62 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -7,17 +7,19 @@ unsafe_op_in_unsafe_fn, clippy::all, clippy::undocumented_unsafe_blocks, - clippy::ptr_cast_constness, - // FIXME(15321): solve CI failures, then replace with `#![expect()]`. - missing_docs + clippy::ptr_cast_constness )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] +//! An implementation of [Mikkelsen's algorithm] for tangent space generation. +//! +//! [Mikkelsen's algorithm]: http://www.mikktspace.com + #[cfg(feature = "std")] extern crate std; diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 82642812b4..4ecf53a773 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_pbr" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Adds PBR rendering to Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -31,22 +31,22 @@ meshlet_processor = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 773852499e..a55403630a 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -37,7 +37,7 @@ mod node; pub mod resources; use bevy_app::{App, Plugin}; -use bevy_asset::load_internal_asset; +use bevy_asset::embedded_asset; use bevy_core_pipeline::core_3d::graph::Node3d; use bevy_ecs::{ component::Component, @@ -49,12 +49,14 @@ use bevy_math::{UVec2, UVec3, Vec3}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_component::UniformComponentPlugin, + load_shader_library, render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines}, + view::Hdr, }; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, TextureFormat, TextureUsages}, + render_resource::{TextureFormat, TextureUsages}, renderer::RenderAdapter, Render, RenderApp, RenderSystems, }; @@ -74,76 +76,21 @@ use self::{ }, }; -mod shaders { - use bevy_asset::{weak_handle, Handle}; - use bevy_render::render_resource::Shader; - - pub const TYPES: Handle = weak_handle!("ef7e147e-30a0-4513-bae3-ddde2a6c20c5"); - pub const FUNCTIONS: Handle = weak_handle!("7ff93872-2ee9-4598-9f88-68b02fef605f"); - pub const BRUNETON_FUNCTIONS: Handle = - weak_handle!("e2dccbb0-7322-444a-983b-e74d0a08bcda"); - pub const BINDINGS: Handle = weak_handle!("bcc55ce5-0fc4-451e-8393-1b9efd2612c4"); - - pub const TRANSMITTANCE_LUT: Handle = - weak_handle!("a4187282-8cb1-42d3-889c-cbbfb6044183"); - pub const MULTISCATTERING_LUT: Handle = - weak_handle!("bde3a71a-73e9-49fe-a379-a81940c67a1e"); - pub const SKY_VIEW_LUT: Handle = weak_handle!("f87e007a-bf4b-4f99-9ef0-ac21d369f0e5"); - pub const AERIAL_VIEW_LUT: Handle = - weak_handle!("a3daf030-4b64-49ae-a6a7-354489597cbe"); - pub const RENDER_SKY: Handle = weak_handle!("09422f46-d0f7-41c1-be24-121c17d6e834"); -} - #[doc(hidden)] pub struct AtmospherePlugin; impl Plugin for AtmospherePlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, shaders::TYPES, "types.wgsl", Shader::from_wgsl); - load_internal_asset!(app, shaders::FUNCTIONS, "functions.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - shaders::BRUNETON_FUNCTIONS, - "bruneton_functions.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "types.wgsl"); + load_shader_library!(app, "functions.wgsl"); + load_shader_library!(app, "bruneton_functions.wgsl"); + load_shader_library!(app, "bindings.wgsl"); - load_internal_asset!(app, shaders::BINDINGS, "bindings.wgsl", Shader::from_wgsl); - - load_internal_asset!( - app, - shaders::TRANSMITTANCE_LUT, - "transmittance_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::MULTISCATTERING_LUT, - "multiscattering_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::SKY_VIEW_LUT, - "sky_view_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::AERIAL_VIEW_LUT, - "aerial_view_lut.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - shaders::RENDER_SKY, - "render_sky.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "transmittance_lut.wgsl"); + embedded_asset!(app, "multiscattering_lut.wgsl"); + embedded_asset!(app, "sky_view_lut.wgsl"); + embedded_asset!(app, "aerial_view_lut.wgsl"); + embedded_asset!(app, "render_sky.wgsl"); app.register_type::() .register_type::() @@ -246,7 +193,7 @@ impl Plugin for AtmospherePlugin { /// from the planet's surface, ozone only exists in a band centered at a fairly /// high altitude. #[derive(Clone, Component, Reflect, ShaderType)] -#[require(AtmosphereSettings)] +#[require(AtmosphereSettings, Hdr)] #[reflect(Clone, Default)] pub struct Atmosphere { /// Radius of the planet @@ -362,7 +309,7 @@ impl ExtractComponent for Atmosphere { type Out = Atmosphere; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } @@ -458,7 +405,7 @@ impl ExtractComponent for AtmosphereSettings { type Out = AtmosphereSettings; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 851447d760..e09b27c590 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -181,7 +181,7 @@ impl ViewNode for RenderSkyNode { view_uniforms_offset, lights_uniforms_offset, render_sky_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index b872916619..d7c93c4418 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,3 +1,4 @@ +use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::Camera3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, }; @@ -21,7 +22,7 @@ use bevy_render::{ use crate::{GpuLights, LightMeta}; -use super::{shaders, Atmosphere, AtmosphereSettings}; +use super::{Atmosphere, AtmosphereSettings}; #[derive(Resource)] pub(crate) struct AtmosphereBindGroupLayouts { @@ -35,6 +36,7 @@ pub(crate) struct AtmosphereBindGroupLayouts { pub(crate) struct RenderSkyBindGroupLayouts { pub render_sky: BindGroupLayout, pub render_sky_msaa: BindGroupLayout, + pub shader: Handle, } impl FromWorld for AtmosphereBindGroupLayouts { @@ -203,6 +205,7 @@ impl FromWorld for RenderSkyBindGroupLayouts { Self { render_sky, render_sky_msaa, + shader: load_embedded_asset!(world, "render_sky.wgsl"), } } } @@ -273,7 +276,7 @@ impl FromWorld for AtmosphereLutPipelines { label: Some("transmittance_lut_pipeline".into()), layout: vec![layouts.transmittance_lut.clone()], push_constant_ranges: vec![], - shader: shaders::TRANSMITTANCE_LUT, + shader: load_embedded_asset!(world, "transmittance_lut.wgsl"), shader_defs: vec![], entry_point: "main".into(), zero_initialize_workgroup_memory: false, @@ -284,7 +287,7 @@ impl FromWorld for AtmosphereLutPipelines { label: Some("multi_scattering_lut_pipeline".into()), layout: vec![layouts.multiscattering_lut.clone()], push_constant_ranges: vec![], - shader: shaders::MULTISCATTERING_LUT, + shader: load_embedded_asset!(world, "multiscattering_lut.wgsl"), shader_defs: vec![], entry_point: "main".into(), zero_initialize_workgroup_memory: false, @@ -294,7 +297,7 @@ impl FromWorld for AtmosphereLutPipelines { label: Some("sky_view_lut_pipeline".into()), layout: vec![layouts.sky_view_lut.clone()], push_constant_ranges: vec![], - shader: shaders::SKY_VIEW_LUT, + shader: load_embedded_asset!(world, "sky_view_lut.wgsl"), shader_defs: vec![], entry_point: "main".into(), zero_initialize_workgroup_memory: false, @@ -304,7 +307,7 @@ impl FromWorld for AtmosphereLutPipelines { label: Some("aerial_view_lut_pipeline".into()), layout: vec![layouts.aerial_view_lut.clone()], push_constant_ranges: vec![], - shader: shaders::AERIAL_VIEW_LUT, + shader: load_embedded_asset!(world, "aerial_view_lut.wgsl"), shader_defs: vec![], entry_point: "main".into(), zero_initialize_workgroup_memory: false, @@ -325,7 +328,6 @@ pub(crate) struct RenderSkyPipelineId(pub CachedRenderPipelineId); #[derive(Copy, Clone, Hash, PartialEq, Eq)] pub(crate) struct RenderSkyPipelineKey { pub msaa_samples: u32, - pub hdr: bool, pub dual_source_blending: bool, } @@ -338,9 +340,6 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { if key.msaa_samples > 1 { shader_defs.push("MULTISAMPLED".into()); } - if key.hdr { - shader_defs.push("TONEMAP_IN_SHADER".into()); - } if key.dual_source_blending { shader_defs.push("DUAL_SOURCE_BLENDING".into()); } @@ -369,7 +368,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { }, zero_initialize_workgroup_memory: false, fragment: Some(FragmentState { - shader: shaders::RENDER_SKY.clone(), + shader: self.shader.clone(), shader_defs, entry_point: "main".into(), targets: vec![Some(ColorTargetState { @@ -394,20 +393,19 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { } pub(super) fn queue_render_sky_pipelines( - views: Query<(Entity, &Camera, &Msaa), With>, + views: Query<(Entity, &Msaa), (With, With)>, pipeline_cache: Res, layouts: Res, mut specializer: ResMut>, render_device: Res, mut commands: Commands, ) { - for (entity, camera, msaa) in &views { + for (entity, msaa) in &views { let id = specializer.specialize( &pipeline_cache, &layouts, RenderSkyPipelineKey { msaa_samples: msaa.samples(), - hdr: camera.hdr, dual_source_blending: render_device .features() .contains(WgpuFeatures::DUAL_SOURCE_BLENDING), @@ -562,7 +560,7 @@ pub(super) fn prepare_atmosphere_transforms( }; for (entity, view) in &views { - let world_from_view = view.world_from_view.compute_matrix(); + let world_from_view = view.world_from_view.to_matrix(); let camera_z = world_from_view.z_axis.truncate(); let camera_y = world_from_view.y_axis.truncate(); let atmo_z = camera_z diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 1b7b3563d7..b0c0fb6347 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -353,7 +353,7 @@ pub(crate) fn assign_objects_to_clusters( let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size); - let world_from_view = camera_transform.compute_matrix(); + let world_from_view = camera_transform.to_matrix(); let view_from_world_scale = camera_transform.compute_transform().scale.recip(); let view_from_world_scale_max = view_from_world_scale.abs().max_element(); let view_from_world = world_from_view.inverse(); diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 3113333be3..3559cb52d5 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -535,12 +535,12 @@ pub fn extract_clusters( continue; } - let num_entities: usize = clusters + let entity_count: usize = clusters .clusterable_objects .iter() .map(|l| l.entities.len()) .sum(); - let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + num_entities); + let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + entity_count); for cluster_objects in &clusters.clusterable_objects { data.push(ExtractedClusterableObjectElement::ClusterHeader( cluster_objects.counts, diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index cadcd1b871..5618b31831 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -17,7 +17,7 @@ use core::{num::NonZero, ops::Deref}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::{AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, @@ -34,10 +34,11 @@ use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, + load_shader_library, render_asset::RenderAssets, render_resource::{ binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler, - SamplerBindingType, Shader, ShaderType, TextureSampleType, TextureView, + SamplerBindingType, ShaderType, TextureSampleType, TextureView, }, renderer::{RenderAdapter, RenderDevice, RenderQueue}, sync_world::RenderEntity, @@ -52,10 +53,6 @@ use crate::{ binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta, LightVisibilityClass, }; -/// The handle to the `clustered.wgsl` shader. -pub(crate) const CLUSTERED_DECAL_SHADER_HANDLE: Handle = - weak_handle!("87929002-3509-42f1-8279-2d2765dd145c"); - /// The maximum number of decals that can be present in a view. /// /// This number is currently relatively low in order to work around the lack of @@ -72,7 +69,7 @@ pub struct ClusteredDecalPlugin; /// An object that projects a decal onto surfaces within its bounds. /// /// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It -/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus +/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus /// you may find [`Transform::looking_at`] useful). /// /// Clustered decals are the highest-quality types of decals that Bevy supports, @@ -152,12 +149,7 @@ impl Default for DecalsBuffer { impl Plugin for ClusteredDecalPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - CLUSTERED_DECAL_SHADER_HANDLE, - "clustered.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "clustered.wgsl"); app.add_plugins(ExtractComponentPlugin::::default()) .register_type::(); diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index 2445c3e723..7d82946346 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -3,10 +3,11 @@ use crate::{ MaterialPlugin, StandardMaterial, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Asset, Assets, Handle}; +use bevy_asset::{weak_handle, Asset, Assets, Handle}; use bevy_ecs::component::Component; use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; use bevy_reflect::{Reflect, TypePath}; +use bevy_render::load_shader_library; use bevy_render::render_asset::RenderAssets; use bevy_render::render_resource::{AsBindGroupShaderType, ShaderType}; use bevy_render::texture::GpuImage; @@ -14,28 +15,20 @@ use bevy_render::{ alpha::AlphaMode, mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}, render_resource::{ - AsBindGroup, CompareFunction, RenderPipelineDescriptor, Shader, - SpecializedMeshPipelineError, + AsBindGroup, CompareFunction, RenderPipelineDescriptor, SpecializedMeshPipelineError, }, RenderDebugFlags, }; const FORWARD_DECAL_MESH_HANDLE: Handle = weak_handle!("afa817f9-1869-4e0c-ac0d-d8cd1552d38a"); -const FORWARD_DECAL_SHADER_HANDLE: Handle = - weak_handle!("f8dfbef4-d88b-42ae-9af4-d9661e9f1648"); /// Plugin to render [`ForwardDecal`]s. pub struct ForwardDecalPlugin; impl Plugin for ForwardDecalPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - FORWARD_DECAL_SHADER_HANDLE, - "forward_decal.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "forward_decal.wgsl"); app.register_type::(); diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index eccf6404ad..28edd38c52 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -10,7 +10,7 @@ use crate::{ ViewLightsUniformOffset, }; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, deferred::{ @@ -34,9 +34,6 @@ use bevy_render::{ pub struct DeferredPbrLightingPlugin; -pub const DEFERRED_LIGHTING_SHADER_HANDLE: Handle = - weak_handle!("f4295279-8890-4748-b654-ca4d2183df1c"); - pub const DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID: u8 = 1; /// Component with a `depth_id` for specifying which corresponding materials should be rendered by this specific PBR deferred lighting pass. @@ -100,12 +97,7 @@ impl Plugin for DeferredPbrLightingPlugin { )) .add_systems(PostUpdate, insert_deferred_lighting_pass_id_component); - load_internal_asset!( - app, - DEFERRED_LIGHTING_SHADER_HANDLE, - "deferred_lighting.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "deferred_lighting.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -237,6 +229,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { pub struct DeferredLightingLayout { mesh_pipeline: MeshPipeline, bind_group_layout_1: BindGroupLayout, + deferred_lighting_shader: Handle, } #[derive(Component)] @@ -360,13 +353,13 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { self.bind_group_layout_1.clone(), ], vertex: VertexState { - shader: DEFERRED_LIGHTING_SHADER_HANDLE, + shader: self.deferred_lighting_shader.clone(), shader_defs: shader_defs.clone(), entry_point: "vertex".into(), buffers: Vec::new(), }, fragment: Some(FragmentState { - shader: DEFERRED_LIGHTING_SHADER_HANDLE, + shader: self.deferred_lighting_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -416,6 +409,7 @@ impl FromWorld for DeferredLightingLayout { Self { mesh_pipeline: world.resource::().clone(), bind_group_layout_1: layout, + deferred_lighting_shader: load_embedded_asset!(world, "deferred_lighting.wgsl"), } } } @@ -455,6 +449,7 @@ pub fn prepare_deferred_lighting_pipelines( ), Has>, Has>, + Has, )>, ) { for ( @@ -467,12 +462,13 @@ pub fn prepare_deferred_lighting_pipelines( (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), has_environment_maps, has_irradiance_volumes, + skip_deferred_lighting, ) in &views { - // If there is no deferred prepass, remove the old pipeline if there was - // one. This handles the case in which a view using deferred stops using - // it. - if !deferred_prepass { + // If there is no deferred prepass or we want to skip the deferred lighting pass, + // remove the old pipeline if there was one. This handles the case in which a + // view using deferred stops using it. + if !deferred_prepass || skip_deferred_lighting { commands.entity(entity).remove::(); continue; } @@ -558,3 +554,14 @@ pub fn prepare_deferred_lighting_pipelines( .insert(DeferredLightingPipeline { pipeline_id }); } } + +/// Component to skip running the deferred lighting pass in [`DeferredOpaquePass3dPbrLightingNode`] for a specific view. +/// +/// This works like [`crate::PbrPlugin::add_default_deferred_lighting_plugin`], but is per-view instead of global. +/// +/// Useful for cases where you want to generate a gbuffer, but skip the built-in deferred lighting pass +/// to run your own custom lighting pass instead. +/// +/// Insert this component in the render world only. +#[derive(Component, Clone, Copy, Default)] +pub struct SkipDeferredLighting; diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index f9226782c9..945bc9c55b 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -2,8 +2,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] extern crate alloc; @@ -124,7 +124,7 @@ pub mod graph { use crate::{deferred::DeferredPbrLightingPlugin, graph::NodePbr}; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, Assets, Handle}; +use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetPath, Assets, Handle}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; use bevy_ecs::prelude::*; use bevy_image::Image; @@ -133,8 +133,9 @@ use bevy_render::{ camera::{sort_cameras, CameraUpdateSystems, Projection}, extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, + load_shader_library, render_graph::RenderGraph, - render_resource::Shader, + render_resource::{Shader, ShaderRef}, sync_component::SyncComponentPlugin, view::VisibilitySystems, ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, @@ -142,40 +143,12 @@ use bevy_render::{ use bevy_transform::TransformSystems; -pub const PBR_TYPES_SHADER_HANDLE: Handle = - weak_handle!("b0330585-2335-4268-9032-a6c4c2d932f6"); -pub const PBR_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("13834c18-c7ec-4c4b-bbbd-432c3ba4cace"); -pub const UTILS_HANDLE: Handle = weak_handle!("0a32978f-2744-4608-98b6-4c3000a0638d"); -pub const CLUSTERED_FORWARD_HANDLE: Handle = - weak_handle!("f8e3b4c6-60b7-4b23-8b2e-a6b27bb4ddce"); -pub const PBR_LIGHTING_HANDLE: Handle = - weak_handle!("de0cf697-2876-49a0-aa0f-f015216f70c2"); -pub const PBR_TRANSMISSION_HANDLE: Handle = - weak_handle!("22482185-36bb-4c16-9b93-a20e6d4a2725"); -pub const SHADOWS_HANDLE: Handle = weak_handle!("ff758c5a-3927-4a15-94c3-3fbdfc362590"); -pub const SHADOW_SAMPLING_HANDLE: Handle = - weak_handle!("f6bf5843-54bc-4e39-bd9d-56bfcd77b033"); -pub const PBR_FRAGMENT_HANDLE: Handle = - weak_handle!("1bd3c10d-851b-400c-934a-db489d99cc50"); -pub const PBR_SHADER_HANDLE: Handle = weak_handle!("0eba65ed-3e5b-4752-93ed-e8097e7b0c84"); -pub const PBR_PREPASS_SHADER_HANDLE: Handle = - weak_handle!("9afeaeab-7c45-43ce-b322-4b97799eaeb9"); -pub const PBR_FUNCTIONS_HANDLE: Handle = - weak_handle!("815b8618-f557-4a96-91a5-a2fb7e249fb0"); -pub const PBR_AMBIENT_HANDLE: Handle = weak_handle!("4a90b95b-112a-4a10-9145-7590d6f14260"); -pub const PARALLAX_MAPPING_SHADER_HANDLE: Handle = - weak_handle!("6cf57d9f-222a-429a-bba4-55ba9586e1d4"); -pub const VIEW_TRANSFORMATIONS_SHADER_HANDLE: Handle = - weak_handle!("ec047703-cde3-4876-94df-fed121544abb"); -pub const PBR_PREPASS_FUNCTIONS_SHADER_HANDLE: Handle = - weak_handle!("77b1bd3a-877c-4b2c-981b-b9c68d1b774a"); -pub const PBR_DEFERRED_TYPES_HANDLE: Handle = - weak_handle!("43060da7-a717-4240-80a8-dbddd92bd25d"); -pub const PBR_DEFERRED_FUNCTIONS_HANDLE: Handle = - weak_handle!("9dc46746-c51d-45e3-a321-6a50c3963420"); -pub const RGB9E5_FUNCTIONS_HANDLE: Handle = - weak_handle!("90c19aa3-6a11-4252-8586-d9299352e94f"); +use std::path::PathBuf; + +fn shader_ref(path: PathBuf) -> ShaderRef { + ShaderRef::Path(AssetPath::from_path_buf(path).with_source("embedded")) +} + const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = weak_handle!("69187376-3dea-4d0f-b3f5-185bde63d6a2"); @@ -211,110 +184,26 @@ impl Default for PbrPlugin { impl Plugin for PbrPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - PBR_TYPES_SHADER_HANDLE, - "render/pbr_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_BINDINGS_SHADER_HANDLE, - "render/pbr_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, UTILS_HANDLE, "render/utils.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - CLUSTERED_FORWARD_HANDLE, - "render/clustered_forward.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_LIGHTING_HANDLE, - "render/pbr_lighting.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_TRANSMISSION_HANDLE, - "render/pbr_transmission.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SHADOWS_HANDLE, - "render/shadows.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_DEFERRED_TYPES_HANDLE, - "deferred/pbr_deferred_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_DEFERRED_FUNCTIONS_HANDLE, - "deferred/pbr_deferred_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SHADOW_SAMPLING_HANDLE, - "render/shadow_sampling.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_FUNCTIONS_HANDLE, - "render/pbr_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - RGB9E5_FUNCTIONS_HANDLE, - "render/rgb9e5.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_AMBIENT_HANDLE, - "render/pbr_ambient.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_FRAGMENT_HANDLE, - "render/pbr_fragment.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - PBR_PREPASS_FUNCTIONS_SHADER_HANDLE, - "render/pbr_prepass_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PBR_PREPASS_SHADER_HANDLE, - "render/pbr_prepass.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - PARALLAX_MAPPING_SHADER_HANDLE, - "render/parallax_mapping.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - VIEW_TRANSFORMATIONS_SHADER_HANDLE, - "render/view_transformations.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "render/pbr_types.wgsl"); + load_shader_library!(app, "render/pbr_bindings.wgsl"); + load_shader_library!(app, "render/utils.wgsl"); + load_shader_library!(app, "render/clustered_forward.wgsl"); + load_shader_library!(app, "render/pbr_lighting.wgsl"); + load_shader_library!(app, "render/pbr_transmission.wgsl"); + load_shader_library!(app, "render/shadows.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_types.wgsl"); + load_shader_library!(app, "deferred/pbr_deferred_functions.wgsl"); + load_shader_library!(app, "render/shadow_sampling.wgsl"); + load_shader_library!(app, "render/pbr_functions.wgsl"); + load_shader_library!(app, "render/rgb9e5.wgsl"); + load_shader_library!(app, "render/pbr_ambient.wgsl"); + load_shader_library!(app, "render/pbr_fragment.wgsl"); + load_shader_library!(app, "render/pbr.wgsl"); + load_shader_library!(app, "render/pbr_prepass_functions.wgsl"); + load_shader_library!(app, "render/pbr_prepass.wgsl"); + load_shader_library!(app, "render/parallax_mapping.wgsl"); + load_shader_library!(app, "render/view_transformations.wgsl"); + // Setup dummy shaders for when MeshletPlugin is not used to prevent shader import errors. load_internal_asset!( app, diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index db255722b3..cfbe99963b 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -48,6 +48,7 @@ impl Default for AmbientLight { } } } + impl AmbientLight { pub const NONE: AmbientLight = AmbientLight { color: Color::WHITE, diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 91ea9cddd3..8273ae4b6d 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -300,22 +300,22 @@ impl From for CascadeShadowConfig { #[reflect(Component, Debug, Default, Clone)] pub struct Cascades { /// Map from a view to the configuration of each of its [`Cascade`]s. - pub(crate) cascades: EntityHashMap>, + pub cascades: EntityHashMap>, } #[derive(Clone, Debug, Default, Reflect)] #[reflect(Clone, Default)] pub struct Cascade { /// The transform of the light, i.e. the view to world matrix. - pub(crate) world_from_cascade: Mat4, + pub world_from_cascade: Mat4, /// The orthographic projection for this cascade. - pub(crate) clip_from_cascade: Mat4, + pub clip_from_cascade: Mat4, /// The view-projection matrix for this cascade, converting world space into light clip space. /// Importantly, this is derived and stored separately from `view_transform` and `projection` to /// ensure shadow stability. - pub(crate) clip_from_world: Mat4, + pub clip_from_world: Mat4, /// Size of each shadow map texel in world units. - pub(crate) texel_size: f32, + pub texel_size: f32, } pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &mut Cascades)>) { @@ -341,7 +341,7 @@ pub fn build_directional_light_cascades( .iter() .filter_map(|(entity, transform, projection, camera)| { if camera.is_active { - Some((entity, projection, transform.compute_matrix())) + Some((entity, projection, transform.to_matrix())) } else { None } @@ -357,7 +357,7 @@ pub fn build_directional_light_cascades( // light_to_world has orthogonal upper-left 3x3 and zero translation. // Even though only the direction (i.e. rotation) of the light matters, we don't constrain // users to not change any other aspects of the transform - there's no guarantee - // `transform.compute_matrix()` will give us a matrix with our desired properties. + // `transform.to_matrix()` will give us a matrix with our desired properties. // Instead, we directly create a good matrix from just the rotation. let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); let light_to_world_inverse = world_from_light.inverse(); @@ -628,7 +628,7 @@ pub fn update_point_light_frusta( for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.compute_matrix().inverse(); + let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); *frustum = Frustum::from_clip_from_world_custom_far( &clip_from_world, diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 52ccaef432..fa55b4d94a 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -44,7 +44,7 @@ //! //! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments -use bevy_asset::{weak_handle, AssetId, Handle}; +use bevy_asset::{AssetId, Handle}; use bevy_ecs::{ component::Component, query::QueryItem, reflect::ReflectComponent, system::lifetimeless::Read, }; @@ -56,8 +56,8 @@ use bevy_render::{ render_asset::RenderAssets, render_resource::{ binding_types::{self, uniform_buffer}, - BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages, - TextureSampleType, TextureView, + BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, ShaderStages, TextureSampleType, + TextureView, }, renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, GpuImage}, @@ -72,10 +72,6 @@ use crate::{ use super::{LightProbeComponent, RenderViewLightProbes}; -/// A handle to the environment map helper shader. -pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle = - weak_handle!("d38c4ec4-e84c-468f-b485-bf44745db937"); - /// A pair of cubemap textures that represent the surroundings of a specific /// area in space. /// @@ -196,7 +192,7 @@ impl ExtractInstance for EnvironmentMapIds { type QueryFilter = (); - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(EnvironmentMapIds { diffuse: item.diffuse_map.id(), specular: item.specular_map.id(), diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 05dd51c379..e2dea463f2 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -17,11 +17,12 @@ //! documentation in the `bevy-baked-gi` project for more details on this //! workflow. //! -//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes that can -//! be arbitrarily scaled, rotated, and positioned in a scene with the -//! [`bevy_transform::components::Transform`] component. The 3D voxel grid will -//! be stretched to fill the interior of the cube, and the illumination from the -//! irradiance volume will apply to all fragments within that bounding region. +//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered +//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a +//! scene with the [`bevy_transform::components::Transform`] component. The 3D +//! voxel grid will be stretched to fill the interior of the cube, with linear +//! interpolation, and the illumination from the irradiance volume will apply to +//! all fragments within that bounding region. //! //! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in //! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of @@ -137,8 +138,8 @@ use bevy_image::Image; use bevy_render::{ render_asset::RenderAssets, render_resource::{ - binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, - TextureSampleType, TextureView, + binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, TextureSampleType, + TextureView, }, renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, GpuImage}, @@ -146,7 +147,7 @@ use bevy_render::{ use bevy_utils::default; use core::{num::NonZero, ops::Deref}; -use bevy_asset::{weak_handle, AssetId, Handle}; +use bevy_asset::{AssetId, Handle}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use crate::{ @@ -154,10 +155,7 @@ use crate::{ MAX_VIEW_LIGHT_PROBES, }; -use super::LightProbeComponent; - -pub const IRRADIANCE_VOLUME_SHADER_HANDLE: Handle = - weak_handle!("7fc7dcd8-3f90-4124-b093-be0e53e08205"); +use super::{LightProbe, LightProbeComponent}; /// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can /// overflow the number of texture bindings when deferred rendering is in use @@ -167,8 +165,12 @@ pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "w /// The component that defines an irradiance volume. /// /// See [`crate::irradiance_volume`] for detailed information. +/// +/// This component requires the [`LightProbe`] component, and is typically used with +/// [`bevy_transform::components::Transform`] to place the volume appropriately. #[derive(Clone, Reflect, Component, Debug)] #[reflect(Component, Default, Debug, Clone)] +#[require(LightProbe)] pub struct IrradianceVolume { /// The 3D texture that represents the ambient cubes, encoded in the format /// described in [`crate::irradiance_volume`]. diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index d7323a1e3c..bfce2f1e26 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -1,7 +1,7 @@ //! Light probes for baked global illumination. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::AssetId; use bevy_core_pipeline::core_3d::Camera3d; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -19,9 +19,10 @@ use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_instances::ExtractInstancesPlugin, + load_shader_library, primitives::{Aabb, Frustum}, render_asset::RenderAssets, - render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView}, + render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView}, renderer::{RenderAdapter, RenderDevice, RenderQueue}, settings::WgpuFeatures, sync_world::RenderEntity, @@ -34,18 +35,10 @@ use tracing::error; use core::{hash::Hash, ops::Deref}; -use crate::{ - irradiance_volume::IRRADIANCE_VOLUME_SHADER_HANDLE, - light_probe::environment_map::{ - EnvironmentMapIds, EnvironmentMapLight, ENVIRONMENT_MAP_SHADER_HANDLE, - }, -}; +use crate::light_probe::environment_map::{EnvironmentMapIds, EnvironmentMapLight}; use self::irradiance_volume::IrradianceVolume; -pub const LIGHT_PROBE_SHADER_HANDLE: Handle = - weak_handle!("e80a2ae6-1c5a-4d9a-a852-d66ff0e6bf7f"); - pub mod environment_map; pub mod irradiance_volume; @@ -344,24 +337,9 @@ pub struct ViewEnvironmentMapUniformOffset(u32); impl Plugin for LightProbePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - LIGHT_PROBE_SHADER_HANDLE, - "light_probe.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - ENVIRONMENT_MAP_SHADER_HANDLE, - "environment_map.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - IRRADIANCE_VOLUME_SHADER_HANDLE, - "irradiance_volume.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "light_probe.wgsl"); + load_shader_library!(app, "environment_map.wgsl"); + load_shader_library!(app, "irradiance_volume.wgsl"); app.register_type::() .register_type::() @@ -400,7 +378,7 @@ fn gather_environment_map_uniform( let environment_map_uniform = if let Some(environment_map_light) = environment_map_light { EnvironmentMapUniform { transform: Transform::from_rotation(environment_map_light.rotation) - .compute_matrix() + .to_matrix() .inverse(), } } else { @@ -617,7 +595,7 @@ where ) -> Option> { environment_map.id(image_assets).map(|id| LightProbeInfo { world_from_light: light_probe_transform.affine(), - light_from_world: light_probe_transform.compute_matrix().inverse(), + light_from_world: light_probe_transform.to_matrix().inverse(), asset_id: id, intensity: environment_map.intensity(), affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 5ddb1d6c72..567bbce674 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -32,14 +32,14 @@ //! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::{AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::RemovedComponents, query::{Changed, Or}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs, system::{Query, Res, ResMut}, @@ -50,8 +50,9 @@ use bevy_math::{uvec2, vec4, Rect, UVec2}; use bevy_platform::collections::HashSet; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ + load_shader_library, render_asset::RenderAssets, - render_resource::{Sampler, Shader, TextureView, WgpuSampler, WgpuTextureView}, + render_resource::{Sampler, TextureView, WgpuSampler, WgpuTextureView}, renderer::RenderAdapter, sync_world::MainEntity, texture::{FallbackImage, GpuImage}, @@ -66,10 +67,6 @@ use tracing::error; use crate::{binding_arrays_are_usable, MeshExtractionSystems}; -/// The ID of the lightmap shader. -pub const LIGHTMAP_SHADER_HANDLE: Handle = - weak_handle!("fc28203f-f258-47f3-973c-ce7d1dd70e59"); - /// The number of lightmaps that we store in a single slab, if bindless textures /// are in use. /// @@ -188,12 +185,7 @@ pub struct LightmapSlotIndex(pub(crate) NonMaxU16); impl Plugin for LightmapPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - LIGHTMAP_SHADER_HANDLE, - "lightmap.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "lightmap.wgsl"); } fn finish(&self, app: &mut App) { diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index b539d2098f..735bc77c99 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -1048,63 +1048,14 @@ where for (bindless_index, owned_binding_resource) in binding_resources.drain(..) { let bindless_index = BindlessIndex(bindless_index); - // If this is an other reference to an object we've already - // allocated, just bump its reference count. - if let Some(pre_existing_resource_slot) = allocation_candidate + + let pre_existing_slot = allocation_candidate .pre_existing_resources - .get(&bindless_index) - { - allocated_resource_slots.insert(bindless_index, *pre_existing_resource_slot); - - match owned_binding_resource { - OwnedBindingResource::Buffer(_) => { - self.buffers - .get_mut(&bindless_index) - .expect("Buffer binding array should exist") - .bindings - .get_mut(*pre_existing_resource_slot as usize) - .and_then(|binding| binding.as_mut()) - .expect("Slot should exist") - .ref_count += 1; - } - - OwnedBindingResource::Data(_) => { - panic!("Data buffers can't be deduplicated") - } - - OwnedBindingResource::TextureView(texture_view_dimension, _) => { - let bindless_resource_type = - BindlessResourceType::from(texture_view_dimension); - self.textures - .get_mut(&bindless_resource_type) - .expect("Texture binding array should exist") - .bindings - .get_mut(*pre_existing_resource_slot as usize) - .and_then(|binding| binding.as_mut()) - .expect("Slot should exist") - .ref_count += 1; - } - - OwnedBindingResource::Sampler(sampler_binding_type, _) => { - let bindless_resource_type = - BindlessResourceType::from(sampler_binding_type); - self.samplers - .get_mut(&bindless_resource_type) - .expect("Sampler binding array should exist") - .bindings - .get_mut(*pre_existing_resource_slot as usize) - .and_then(|binding| binding.as_mut()) - .expect("Slot should exist") - .ref_count += 1; - } - } - - continue; - } + .get(&bindless_index); // Otherwise, we need to insert it anew. let binding_resource_id = BindingResourceId::from(&owned_binding_resource); - match owned_binding_resource { + let increment_allocated_resource_count = match owned_binding_resource { OwnedBindingResource::Buffer(buffer) => { let slot = self .buffers @@ -1112,14 +1063,27 @@ where .expect("Buffer binding array should exist") .insert(binding_resource_id, buffer); allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } } OwnedBindingResource::Data(data) => { + if pre_existing_slot.is_some() { + panic!("Data buffers can't be deduplicated") + } + let slot = self .data_buffers .get_mut(&bindless_index) .expect("Data buffer binding array should exist") .insert(&data); allocated_resource_slots.insert(bindless_index, slot); + false } OwnedBindingResource::TextureView(texture_view_dimension, texture_view) => { let bindless_resource_type = BindlessResourceType::from(texture_view_dimension); @@ -1129,6 +1093,14 @@ where .expect("Texture array should exist") .insert(binding_resource_id, texture_view); allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } } OwnedBindingResource::Sampler(sampler_binding_type, sampler) => { let bindless_resource_type = BindlessResourceType::from(sampler_binding_type); @@ -1138,11 +1110,21 @@ where .expect("Sampler should exist") .insert(binding_resource_id, sampler); allocated_resource_slots.insert(bindless_index, slot); + + if let Some(pre_existing_slot) = pre_existing_slot { + assert_eq!(*pre_existing_slot, slot); + + false + } else { + true + } } - } + }; // Bump the allocated resource count. - self.allocated_resource_count += 1; + if increment_allocated_resource_count { + self.allocated_resource_count += 1; + } } allocated_resource_slots @@ -1626,16 +1608,30 @@ where /// Inserts a bindless resource into a binding array and returns the index /// of the slot it was inserted into. fn insert(&mut self, binding_resource_id: BindingResourceId, resource: R) -> u32 { - let slot = self.free_slots.pop().unwrap_or(self.len); - self.resource_to_slot.insert(binding_resource_id, slot); + match self.resource_to_slot.entry(binding_resource_id) { + bevy_platform::collections::hash_map::Entry::Occupied(o) => { + let slot = *o.get(); - if self.bindings.len() < slot as usize + 1 { - self.bindings.resize_with(slot as usize + 1, || None); + self.bindings[slot as usize] + .as_mut() + .expect("A slot in the resource_to_slot map should have a value") + .ref_count += 1; + + slot + } + bevy_platform::collections::hash_map::Entry::Vacant(v) => { + let slot = self.free_slots.pop().unwrap_or(self.len); + v.insert(slot); + + if self.bindings.len() < slot as usize + 1 { + self.bindings.resize_with(slot as usize + 1, || None); + } + self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); + + self.len += 1; + slot + } } - self.bindings[slot as usize] = Some(MaterialBindlessBinding::new(resource)); - - self.len += 1; - slot } /// Removes a reference to an object from the slot. diff --git a/crates/bevy_pbr/src/parallax.rs b/crates/bevy_pbr/src/parallax.rs index 0a847b7c25..be588ca87c 100644 --- a/crates/bevy_pbr/src/parallax.rs +++ b/crates/bevy_pbr/src/parallax.rs @@ -33,6 +33,7 @@ pub enum ParallaxMappingMethod { max_steps: u32, }, } + impl ParallaxMappingMethod { /// [`ParallaxMappingMethod::Relief`] with a 5 steps, a reasonable default. pub const DEFAULT_RELIEF_MAPPING: Self = ParallaxMappingMethod::Relief { max_steps: 5 }; diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index fd1babd8ec..cbd8445483 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1345,7 +1345,7 @@ impl From<&StandardMaterial> for StandardMaterialKey { impl Material for StandardMaterial { fn fragment_shader() -> ShaderRef { - PBR_SHADER_HANDLE.into() + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) } #[inline] @@ -1381,11 +1381,11 @@ impl Material for StandardMaterial { } fn prepass_fragment_shader() -> ShaderRef { - PBR_PREPASS_SHADER_HANDLE.into() + shader_ref(bevy_asset::embedded_path!("render/pbr_prepass.wgsl")) } fn deferred_fragment_shader() -> ShaderRef { - PBR_SHADER_HANDLE.into() + shader_ref(bevy_asset::embedded_path!("render/pbr.wgsl")) } #[cfg(feature = "meshlet")] diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index cc8dcea6a0..aef2b74177 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -13,6 +13,7 @@ use bevy_app::{App, Plugin, PreUpdate}; use bevy_render::{ alpha::AlphaMode, batching::gpu_preprocessing::GpuPreprocessingSupport, + load_shader_library, mesh::{allocator::MeshAllocator, Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::prepare_assets, render_resource::binding_types::uniform_buffer, @@ -23,7 +24,7 @@ use bevy_render::{ }; pub use prepass_bindings::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetServer, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prelude::Camera3d, prepass::*, }; @@ -63,18 +64,6 @@ use bevy_render::view::RenderVisibleEntities; use bevy_render::RenderSystems::{PrepareAssets, PrepareResources}; use core::{hash::Hash, marker::PhantomData}; -pub const PREPASS_SHADER_HANDLE: Handle = - weak_handle!("ce810284-f1ae-4439-ab2e-0d6b204b6284"); - -pub const PREPASS_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("3e83537e-ae17-489c-a18a-999bc9c1d252"); - -pub const PREPASS_UTILS_SHADER_HANDLE: Handle = - weak_handle!("02e4643a-a14b-48eb-a339-0c47aeab0d7e"); - -pub const PREPASS_IO_SHADER_HANDLE: Handle = - weak_handle!("1c065187-c99b-4b7c-ba59-c1575482d2c9"); - /// Sets up everything required to use the prepass pipeline. /// /// This does not add the actual prepasses, see [`PrepassPlugin`] for that. @@ -91,33 +80,11 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - PREPASS_SHADER_HANDLE, - "prepass.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "prepass.wgsl"); - load_internal_asset!( - app, - PREPASS_BINDINGS_SHADER_HANDLE, - "prepass_bindings.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - PREPASS_UTILS_SHADER_HANDLE, - "prepass_utils.wgsl", - Shader::from_wgsl - ); - - load_internal_asset!( - app, - PREPASS_IO_SHADER_HANDLE, - "prepass_io.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "prepass_bindings.wgsl"); + load_shader_library!(app, "prepass_utils.wgsl"); + load_shader_library!(app, "prepass_io.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -249,11 +216,16 @@ pub fn update_previous_view_data( query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, ) { for (entity, camera, camera_transform) in &query { - let view_from_world = camera_transform.compute_matrix().inverse(); + let world_from_view = camera_transform.to_matrix(); + let view_from_world = world_from_view.inverse(); + let view_from_clip = camera.clip_from_view().inverse(); + commands.entity(entity).try_insert(PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view() * view_from_world, clip_from_view: camera.clip_from_view(), + world_from_clip: world_from_view * view_from_clip, + view_from_clip, }); } } @@ -305,6 +277,7 @@ pub struct PrepassPipelineInternal { pub prepass_material_fragment_shader: Option>, pub deferred_material_vertex_shader: Option>, pub deferred_material_fragment_shader: Option>, + pub default_prepass_shader: Handle, /// Whether skins will use uniform buffers on account of storage buffers /// being unavailable on this platform. @@ -403,6 +376,7 @@ impl FromWorld for PrepassPipeline { ShaderRef::Handle(handle) => Some(handle), ShaderRef::Path(path) => Some(asset_server.load(path)), }, + default_prepass_shader: load_embedded_asset!(world, "prepass.wgsl"), material_layout: M::bind_group_layout(render_device), skins_use_uniform_buffers: skin::skins_use_uniform_buffers(render_device), depth_clip_control_supported, @@ -610,12 +584,12 @@ impl PrepassPipelineInternal { let frag_shader_handle = if mesh_key.contains(MeshPipelineKey::DEFERRED_PREPASS) { match self.deferred_material_fragment_shader.clone() { Some(frag_shader_handle) => frag_shader_handle, - _ => PREPASS_SHADER_HANDLE, + None => self.default_prepass_shader.clone(), } } else { match self.prepass_material_fragment_shader.clone() { Some(frag_shader_handle) => frag_shader_handle, - _ => PREPASS_SHADER_HANDLE, + None => self.default_prepass_shader.clone(), } }; @@ -632,12 +606,12 @@ impl PrepassPipelineInternal { if let Some(handle) = &self.deferred_material_vertex_shader { handle.clone() } else { - PREPASS_SHADER_HANDLE + self.default_prepass_shader.clone() } } else if let Some(handle) = &self.prepass_material_vertex_shader { handle.clone() } else { - PREPASS_SHADER_HANDLE + self.default_prepass_shader.clone() }; let descriptor = RenderPipelineDescriptor { vertex: VertexState { @@ -729,11 +703,16 @@ pub fn prepare_previous_view_uniforms( let prev_view_data = match maybe_previous_view_uniforms { Some(previous_view) => previous_view.clone(), None => { - let view_from_world = camera.world_from_view.compute_matrix().inverse(); + let world_from_view = camera.world_from_view.to_matrix(); + let view_from_world = world_from_view.inverse(); + let view_from_clip = camera.clip_from_view.inverse(); + PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view * view_from_world, clip_from_view: camera.clip_from_view, + world_from_clip: world_from_view * view_from_clip, + view_from_clip, } } }; @@ -983,12 +962,18 @@ pub fn specialize_prepass_material_meshes( AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add - | AlphaMode::Multiply => continue, + | AlphaMode::Multiply => { + // In case this material was previously in a valid alpha_mode, remove it to + // stop the queue system from assuming its retained cache to be valid. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } } if material.properties.reads_view_transmission_texture { // No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d` // phase, and are therefore also excluded from the prepass much like alpha-blended materials. + view_specialized_material_pipeline_cache.remove(visible_entity); continue; } diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl index 3bd27b2e03..141f7d7b0d 100644 --- a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -4,6 +4,8 @@ struct PreviousViewUniforms { view_from_world: mat4x4, clip_from_world: mat4x4, clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, } @group(0) @binding(2) var previous_view_uniforms: PreviousViewUniforms; diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 9d7fd0b18d..fa09120725 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -1,11 +1,11 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; use bevy_math::{Vec3, Vec4}; use bevy_render::{ extract_component::ExtractComponentPlugin, - render_resource::{DynamicUniformBuffer, Shader, ShaderType}, + load_shader_library, + render_resource::{DynamicUniformBuffer, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ExtractedView, Render, RenderApp, RenderSystems, @@ -126,15 +126,12 @@ pub struct ViewFogUniformOffset { pub offset: u32, } -/// Handle for the fog WGSL Shader internal asset -pub const FOG_SHADER_HANDLE: Handle = weak_handle!("e943f446-2856-471c-af5e-68dd276eec42"); - /// A plugin that consolidates fog extraction, preparation and related resources/assets pub struct FogPlugin; impl Plugin for FogPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, FOG_SHADER_HANDLE, "fog.wgsl", Shader::from_wgsl); + load_shader_library!(app, "fog.wgsl"); app.register_type::(); app.add_plugins(ExtractComponentPlugin::::default()); diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 5356c7580e..eaa7e857b7 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -9,7 +9,7 @@ use core::num::{NonZero, NonZeroU64}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, experimental::mip_generation::ViewDepthPyramid, @@ -63,16 +63,6 @@ use crate::{ use super::{ShadowView, ViewLightEntities}; -/// The handle to the `mesh_preprocess.wgsl` compute shader. -pub const MESH_PREPROCESS_SHADER_HANDLE: Handle = - weak_handle!("c8579292-cf92-43b5-9c5a-ec5bd4e44d12"); -/// The handle to the `reset_indirect_batch_sets.wgsl` compute shader. -pub const RESET_INDIRECT_BATCH_SETS_SHADER_HANDLE: Handle = - weak_handle!("045fb176-58e2-4e76-b241-7688d761bb23"); -/// The handle to the `build_indirect_params.wgsl` compute shader. -pub const BUILD_INDIRECT_PARAMS_SHADER_HANDLE: Handle = - weak_handle!("133b01f0-3eaf-4590-9ee9-f0cf91a00b71"); - /// The GPU workgroup size. const WORKGROUP_SIZE: usize = 64; @@ -255,6 +245,8 @@ pub struct PreprocessPhasePipelines { pub struct PreprocessPipeline { /// The bind group layout for the compute shader. pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, /// The pipeline ID for the compute shader. /// /// This gets filled in `prepare_preprocess_pipelines`. @@ -269,6 +261,8 @@ pub struct PreprocessPipeline { pub struct ResetIndirectBatchSetsPipeline { /// The bind group layout for the compute shader. pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, /// The pipeline ID for the compute shader. /// /// This gets filled in `prepare_preprocess_pipelines`. @@ -280,6 +274,8 @@ pub struct ResetIndirectBatchSetsPipeline { pub struct BuildIndirectParametersPipeline { /// The bind group layout for the compute shader. pub bind_group_layout: BindGroupLayout, + /// The shader asset handle. + pub shader: Handle, /// The pipeline ID for the compute shader. /// /// This gets filled in `prepare_preprocess_pipelines`. @@ -431,24 +427,9 @@ pub struct SkipGpuPreprocess; impl Plugin for GpuMeshPreprocessPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - MESH_PREPROCESS_SHADER_HANDLE, - "mesh_preprocess.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - RESET_INDIRECT_BATCH_SETS_SHADER_HANDLE, - "reset_indirect_batch_sets.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - BUILD_INDIRECT_PARAMS_SHADER_HANDLE, - "build_indirect_params.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "mesh_preprocess.wgsl"); + embedded_asset!(app, "reset_indirect_batch_sets.wgsl"); + embedded_asset!(app, "build_indirect_params.wgsl"); } fn finish(&self, app: &mut App) { @@ -1292,7 +1273,7 @@ impl SpecializedComputePipeline for PreprocessPipeline { } else { vec![] }, - shader: MESH_PREPROCESS_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "main".into(), zero_initialize_workgroup_memory: false, @@ -1363,18 +1344,27 @@ impl FromWorld for PreprocessPipelines { &build_non_indexed_indirect_params_bind_group_layout_entries, ); + let preprocess_shader = load_embedded_asset!(world, "mesh_preprocess.wgsl"); + let reset_indirect_batch_sets_shader = + load_embedded_asset!(world, "reset_indirect_batch_sets.wgsl"); + let build_indirect_params_shader = + load_embedded_asset!(world, "build_indirect_params.wgsl"); + let preprocess_phase_pipelines = PreprocessPhasePipelines { reset_indirect_batch_sets: ResetIndirectBatchSetsPipeline { bind_group_layout: reset_indirect_batch_sets_bind_group_layout.clone(), + shader: reset_indirect_batch_sets_shader, pipeline_id: None, }, gpu_occlusion_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), pipeline_id: None, }, gpu_occlusion_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), pipeline_id: None, }, }; @@ -1382,27 +1372,33 @@ impl FromWorld for PreprocessPipelines { PreprocessPipelines { direct_preprocess: PreprocessPipeline { bind_group_layout: direct_bind_group_layout, + shader: preprocess_shader.clone(), pipeline_id: None, }, gpu_frustum_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_frustum_culling_bind_group_layout, + shader: preprocess_shader.clone(), pipeline_id: None, }, early_gpu_occlusion_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_early_occlusion_culling_bind_group_layout, + shader: preprocess_shader.clone(), pipeline_id: None, }, late_gpu_occlusion_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_late_occlusion_culling_bind_group_layout, + shader: preprocess_shader, pipeline_id: None, }, gpu_frustum_culling_build_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader.clone(), pipeline_id: None, }, gpu_frustum_culling_build_non_indexed_indirect_params: BuildIndirectParametersPipeline { bind_group_layout: build_non_indexed_indirect_params_bind_group_layout.clone(), + shader: build_indirect_params_shader, pipeline_id: None, }, early_phase: preprocess_phase_pipelines.clone(), @@ -1642,7 +1638,7 @@ impl SpecializedComputePipeline for ResetIndirectBatchSetsPipeline { label: Some("reset indirect batch sets".into()), layout: vec![self.bind_group_layout.clone()], push_constant_ranges: vec![], - shader: RESET_INDIRECT_BATCH_SETS_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs: vec![], entry_point: "main".into(), zero_initialize_workgroup_memory: false, @@ -1696,7 +1692,7 @@ impl SpecializedComputePipeline for BuildIndirectParametersPipeline { label: Some(label.into()), layout: vec![self.bind_group_layout.clone()], push_constant_ranges: vec![], - shader: BUILD_INDIRECT_PARAMS_SHADER_HANDLE, + shader: self.shader.clone(), 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 d71dccc71a..83d28a7da7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -121,7 +121,6 @@ pub struct GpuDirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, - skip: u32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -220,7 +219,18 @@ pub fn extract_lights( mut commands: Commands, point_light_shadow_map: Extract>, directional_light_shadow_map: Extract>, - global_point_lights: Extract>, + global_visible_clusterable: Extract>, + previous_point_lights: Query< + Entity, + ( + With, + With, + ), + >, + previous_spot_lights: Query< + Entity, + (With, With), + >, point_lights: Extract< Query<( Entity, @@ -276,6 +286,22 @@ pub fn extract_lights( if directional_light_shadow_map.is_changed() { commands.insert_resource(directional_light_shadow_map.clone()); } + + // Clear previous visible entities for all point/spot lights as they might not be in the + // `global_visible_clusterable` list anymore. + commands.try_insert_batch( + previous_point_lights + .iter() + .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) + .collect::>(), + ); + commands.try_insert_batch( + previous_spot_lights + .iter() + .map(|render_entity| (render_entity, RenderVisibleMeshEntities::default())) + .collect::>(), + ); + // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 // world unit from the light. // point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels @@ -286,7 +312,7 @@ pub fn extract_lights( let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32; let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); - for entity in global_point_lights.iter().copied() { + for entity in global_visible_clusterable.iter().copied() { let Ok(( main_entity, render_entity, @@ -350,7 +376,7 @@ pub fn extract_lights( commands.try_insert_batch(point_lights_values); let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); - for entity in global_point_lights.iter().copied() { + for entity in global_visible_clusterable.iter().copied() { if let Ok(( main_entity, render_entity, @@ -522,7 +548,7 @@ pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { if let Ok(mut v) = commands.get_entity(trigger.target()) { @@ -532,7 +558,7 @@ pub(crate) fn add_light_view_entities( /// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. pub(crate) fn extracted_light_removed( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { if let Ok(mut v) = commands.get_entity(trigger.target()) { @@ -541,7 +567,7 @@ pub(crate) fn extracted_light_removed( } pub(crate) fn remove_light_view_entities( - trigger: Trigger, + trigger: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { @@ -982,57 +1008,37 @@ pub fn prepare_lights( global_light_meta.entity_to_index.insert(entity, index); } - let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; + // iterate the views once to find the maximum number of cascade shadowmaps we will need let mut num_directional_cascades_enabled = 0usize; - for (index, (_light_entity, _, light)) in directional_lights + for ( + _entity, + _camera_main_entity, + _extracted_view, + _clusters, + maybe_layers, + _no_indirect_drawing, + _maybe_ambient_override, + ) in sorted_cameras + .0 .iter() - .enumerate() - .take(MAX_DIRECTIONAL_LIGHTS) + .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) { - let mut flags = DirectionalLightFlags::NONE; + let mut num_directional_cascades_for_this_view = 0usize; + let render_layers = maybe_layers.unwrap_or_default(); - // Lights are sorted, volumetric and shadow enabled lights are first - if light.volumetric - && light.shadows_enabled - && (index < directional_volumetric_enabled_count) - { - flags |= DirectionalLightFlags::VOLUMETRIC; - } - // Shadow enabled lights are second - if light.shadows_enabled && (index < directional_shadow_enabled_count) { - flags |= DirectionalLightFlags::SHADOWS_ENABLED; + for (_light_entity, _, light) in directional_lights.iter() { + if light.shadows_enabled && light.render_layers.intersects(render_layers) { + num_directional_cascades_for_this_view += light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + } } - if light.affects_lightmapped_mesh_diffuse { - flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; - } - - let num_cascades = light - .cascade_shadow_config - .bounds - .len() - .min(MAX_CASCADES_PER_LIGHT); - gpu_directional_lights[index] = GpuDirectionalLight { - // Set to true later when necessary. - skip: 0u32, - // Filled in later. - cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], - // premultiply color by illuminance - // we don't use the alpha at all, so no reason to multiply only [0..3] - color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, - // direction is negated to be ready for N.L - dir_to_light: light.transform.back().into(), - flags: flags.bits(), - soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), - shadow_depth_bias: light.shadow_depth_bias, - shadow_normal_bias: light.shadow_normal_bias, - num_cascades: num_cascades as u32, - cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, - depth_texture_base_index: num_directional_cascades_enabled as u32, - }; - if index < directional_shadow_enabled_count { - num_directional_cascades_enabled += num_cascades; - } + num_directional_cascades_enabled = num_directional_cascades_enabled + .max(num_directional_cascades_for_this_view) + .min(max_texture_array_layers); } global_light_meta @@ -1157,6 +1163,7 @@ pub fn prepare_lights( { live_views.insert(entity); + let view_layers = maybe_layers.unwrap_or_default(); let mut view_lights = Vec::new(); let mut view_occlusion_culling_lights = Vec::new(); @@ -1176,6 +1183,68 @@ pub fn prepare_lights( let n_clusters = clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z; let ambient_light = maybe_ambient_override.unwrap_or(&ambient_light); + + let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; + let mut num_directional_cascades_enabled_for_this_view = 0usize; + let mut num_directional_lights_for_this_view = 0usize; + for (index, (_light_entity, _, light)) in directional_lights + .iter() + .filter(|(_light_entity, _, light)| light.render_layers.intersects(view_layers)) + .enumerate() + .take(MAX_DIRECTIONAL_LIGHTS) + { + num_directional_lights_for_this_view += 1; + + let mut flags = DirectionalLightFlags::NONE; + + // Lights are sorted, volumetric and shadow enabled lights are first + if light.volumetric + && light.shadows_enabled + && (index < directional_volumetric_enabled_count) + { + flags |= DirectionalLightFlags::VOLUMETRIC; + } + + // Shadow enabled lights are second + let mut num_cascades = 0; + if light.shadows_enabled { + let cascades = light + .cascade_shadow_config + .bounds + .len() + .min(MAX_CASCADES_PER_LIGHT); + + if num_directional_cascades_enabled_for_this_view + cascades + <= max_texture_array_layers + { + flags |= DirectionalLightFlags::SHADOWS_ENABLED; + num_cascades += cascades; + } + } + + if light.affects_lightmapped_mesh_diffuse { + flags |= DirectionalLightFlags::AFFECTS_LIGHTMAPPED_MESH_DIFFUSE; + } + + gpu_directional_lights[index] = GpuDirectionalLight { + // Filled in later. + cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], + // premultiply color by illuminance + // we don't use the alpha at all, so no reason to multiply only [0..3] + color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, + // direction is negated to be ready for N.L + dir_to_light: light.transform.back().into(), + flags: flags.bits(), + soft_shadow_size: light.soft_shadow_size.unwrap_or_default(), + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + num_cascades: num_cascades as u32, + cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, + depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32, + }; + num_directional_cascades_enabled_for_this_view += num_cascades; + } + let mut gpu_lights = GpuLights { directional_lights: gpu_directional_lights, ambient_color: Vec4::from_slice(&LinearRgba::from(ambient_light.color).to_f32_array()) @@ -1187,8 +1256,7 @@ pub fn prepare_lights( cluster_factors_zw.y, ), cluster_dimensions: clusters.dimensions.extend(n_clusters), - n_directional_lights: directional_lights.iter().len().min(MAX_DIRECTIONAL_LIGHTS) - as u32, + n_directional_lights: num_directional_lights_for_this_view as u32, // spotlight shadow maps are stored in the directional light array, starting at num_directional_cascades_enabled. // the spot lights themselves start in the light array at point_light_count. so to go from light // index to shadow map index, we need to subtract point light count and add directional shadowmap count. @@ -1418,27 +1486,31 @@ pub fn prepare_lights( } // directional lights + // clear entities for lights that don't intersect the layer + for &(light_entity, _, _) in directional_lights + .iter() + .filter(|(_, _, light)| !light.render_layers.intersects(view_layers)) + { + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + if let Some(entities) = light_view_entities.remove(&entity) { + despawn_entities(&mut commands, entities); + } + } + let mut directional_depth_texture_array_index = 0u32; - let view_layers = maybe_layers.unwrap_or_default(); for (light_index, &(light_entity, light_main_entity, light)) in directional_lights .iter() + .filter(|(_, _, light)| light.render_layers.intersects(view_layers)) .enumerate() .take(MAX_DIRECTIONAL_LIGHTS) { - let gpu_light = &mut gpu_lights.directional_lights[light_index]; - let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { continue; }; - // Check if the light intersects with the view. - if !view_layers.intersects(&light.render_layers) { - gpu_light.skip = 1u32; - if let Some(entities) = light_view_entities.remove(&entity) { - despawn_entities(&mut commands, entities); - } - continue; - } + let gpu_light = &mut gpu_lights.directional_lights[light_index]; // Only deal with cascades when shadows are enabled. if (gpu_light.flags & DirectionalLightFlags::SHADOWS_ENABLED.bits()) == 0u32 { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 11abee8746..d8a3256b13 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,6 +1,6 @@ use crate::material_bind_groups::{MaterialBindGroupIndex, MaterialBindGroupSlot}; use allocator::MeshAllocator; -use bevy_asset::{load_internal_asset, AssetId}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, @@ -101,22 +101,6 @@ impl MeshRenderPlugin { } } -pub const FORWARD_IO_HANDLE: Handle = weak_handle!("38111de1-6e35-4dbb-877b-7b6f9334baf6"); -pub const MESH_VIEW_TYPES_HANDLE: Handle = - weak_handle!("979493db-4ae1-4003-b5c6-fcbb88b152a2"); -pub const MESH_VIEW_BINDINGS_HANDLE: Handle = - weak_handle!("c6fe674b-4c21-4d4b-867a-352848da5337"); -pub const MESH_TYPES_HANDLE: Handle = weak_handle!("a4a3fc2e-a57e-4083-a8ab-2840176927f2"); -pub const MESH_BINDINGS_HANDLE: Handle = - weak_handle!("84e7f9e6-e566-4a61-914e-c568f5dabf49"); -pub const MESH_FUNCTIONS_HANDLE: Handle = - weak_handle!("c46aa0f0-6c0c-4b3a-80bf-d8213c771f12"); -pub const MESH_SHADER_HANDLE: Handle = weak_handle!("1a7bbae8-4b4f-48a7-b53b-e6822e56f321"); -pub const SKINNING_HANDLE: Handle = weak_handle!("7474e812-2506-4cbf-9de3-fe07e5c6ff24"); -pub const MORPH_HANDLE: Handle = weak_handle!("da30aac7-34cc-431d-a07f-15b1a783008c"); -pub const OCCLUSION_CULLING_HANDLE: Handle = - weak_handle!("eaea07d9-7516-482c-aa42-6f8e9927e1f0"); - /// How many textures are allowed in the view bind group layout (`@group(0)`) before /// broader compatibility with WebGL and WebGPU is at risk, due to the minimum guaranteed /// values for `MAX_TEXTURE_IMAGE_UNITS` (in WebGL) and `maxSampledTexturesPerShaderStage` (in WebGPU), @@ -130,45 +114,28 @@ pub const MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES: usize = 10; impl Plugin for MeshRenderPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, FORWARD_IO_HANDLE, "forward_io.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - MESH_VIEW_TYPES_HANDLE, - "mesh_view_types.wgsl", - Shader::from_wgsl_with_defs, - vec![ - ShaderDefVal::UInt( - "MAX_DIRECTIONAL_LIGHTS".into(), - MAX_DIRECTIONAL_LIGHTS as u32 - ), - ShaderDefVal::UInt( - "MAX_CASCADES_PER_LIGHT".into(), - MAX_CASCADES_PER_LIGHT as u32, - ) - ] - ); - load_internal_asset!( - app, - MESH_VIEW_BINDINGS_HANDLE, - "mesh_view_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH_TYPES_HANDLE, "mesh_types.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - MESH_FUNCTIONS_HANDLE, - "mesh_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH_SHADER_HANDLE, "mesh.wgsl", Shader::from_wgsl); - load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl); - load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - OCCLUSION_CULLING_HANDLE, - "occlusion_culling.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "forward_io.wgsl"); + load_shader_library!(app, "mesh_view_types.wgsl", |settings| *settings = + ShaderSettings { + shader_defs: vec![ + ShaderDefVal::UInt( + "MAX_DIRECTIONAL_LIGHTS".into(), + MAX_DIRECTIONAL_LIGHTS as u32 + ), + ShaderDefVal::UInt( + "MAX_CASCADES_PER_LIGHT".into(), + MAX_CASCADES_PER_LIGHT as u32, + ) + ] + }); + load_shader_library!(app, "mesh_view_bindings.wgsl"); + load_shader_library!(app, "mesh_types.wgsl"); + load_shader_library!(app, "mesh_functions.wgsl"); + load_shader_library!(app, "skinning.wgsl"); + load_shader_library!(app, "morph.wgsl"); + load_shader_library!(app, "occlusion_culling.wgsl"); + + embedded_asset!(app, "mesh.wgsl"); if app.get_sub_app(RenderApp).is_none() { return; @@ -310,13 +277,10 @@ impl Plugin for MeshRenderPlugin { // Load the mesh_bindings shader module here as it depends on runtime information about // whether storage buffers are supported, or the maximum uniform buffer binding size. - load_internal_asset!( - app, - MESH_BINDINGS_HANDLE, - "mesh_bindings.wgsl", - Shader::from_wgsl_with_defs, - mesh_bindings_shader_defs - ); + load_shader_library!(app, "mesh_bindings.wgsl", move |settings| *settings = + ShaderSettings { + shader_defs: mesh_bindings_shader_defs.clone(), + }); } } @@ -837,12 +801,33 @@ pub struct RenderMeshInstanceGpuQueues(Parallel); pub struct MeshesToReextractNextFrame(MainEntityHashSet); impl RenderMeshInstanceShared { - fn from_components( + /// A gpu builder will provide the mesh instance id + /// during [`RenderMeshInstanceGpuBuilder::update`]. + fn for_gpu_building( previous_transform: Option<&PreviousGlobalTransform>, mesh: &Mesh3d, tag: Option<&MeshTag>, not_shadow_caster: bool, no_automatic_batching: bool, + ) -> Self { + Self::for_cpu_building( + previous_transform, + mesh, + tag, + default(), + not_shadow_caster, + no_automatic_batching, + ) + } + + /// The cpu builder does not have an equivalent [`RenderMeshInstanceGpuBuilder::update`]. + fn for_cpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + material_bindings_index: MaterialBindingId, + not_shadow_caster: bool, + no_automatic_batching: bool, ) -> Self { let mut mesh_instance_flags = RenderMeshInstanceFlags::empty(); mesh_instance_flags.set(RenderMeshInstanceFlags::SHADOW_CASTER, !not_shadow_caster); @@ -858,8 +843,7 @@ impl RenderMeshInstanceShared { RenderMeshInstanceShared { mesh_asset_id: mesh.id(), flags: mesh_instance_flags, - // This gets filled in later, during `RenderMeshGpuBuilder::update`. - material_bindings_index: default(), + material_bindings_index, lightmap_slab_index: None, tag: tag.map_or(0, |i| **i), } @@ -1309,6 +1293,8 @@ pub type ExtractMeshesSet = MeshExtractionSystems; /// [`MeshUniform`] building. pub fn extract_meshes_for_cpu_building( mut render_mesh_instances: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, render_visibility_ranges: Res, mut render_mesh_instance_queues: Local>>, meshes_query: Extract< @@ -1362,10 +1348,18 @@ pub fn extract_meshes_for_cpu_building( transmitted_receiver, ); - let shared = RenderMeshInstanceShared::from_components( + let mesh_material = mesh_material_ids.mesh_material(MainEntity::from(entity)); + + let material_bindings_index = render_material_bindings + .get(&mesh_material) + .copied() + .unwrap_or_default(); + + let shared = RenderMeshInstanceShared::for_cpu_building( previous_transform, mesh, tag, + material_bindings_index, not_shadow_caster, no_automatic_batching, ); @@ -1546,7 +1540,7 @@ fn extract_mesh_for_gpu_building( not_shadow_caster, no_automatic_batching, visibility_range, - ): ::Item<'_>, + ): ::Item<'_, '_>, render_visibility_ranges: &RenderVisibilityRanges, render_mesh_instances: &RenderMeshInstancesGpu, queue: &mut RenderMeshInstanceGpuQueue, @@ -1570,7 +1564,7 @@ fn extract_mesh_for_gpu_building( transmitted_receiver, ); - let shared = RenderMeshInstanceShared::from_components( + let shared = RenderMeshInstanceShared::for_gpu_building( previous_transform, mesh, tag, @@ -1757,6 +1751,8 @@ pub struct MeshPipeline { pub dummy_white_gpu_image: GpuImage, pub clustered_forward_buffer_binding_type: BufferBindingType, pub mesh_layouts: MeshLayouts, + /// The shader asset handle. + pub shader: Handle, /// `MeshUniform`s are stored in arrays in buffers. If storage buffers are available, they /// are used and this will be `None`, otherwise uniform buffers will be used with batches /// of this many `MeshUniform`s, stored at dynamic offsets within the uniform buffer. @@ -1786,6 +1782,7 @@ pub struct MeshPipeline { impl FromWorld for MeshPipeline { fn from_world(world: &mut World) -> Self { + let shader = load_embedded_asset!(world, "mesh.wgsl"); let mut system_state: SystemState<( Res, Res, @@ -1838,6 +1835,7 @@ impl FromWorld for MeshPipeline { clustered_forward_buffer_binding_type, dummy_white_gpu_image, mesh_layouts: MeshLayouts::new(&render_device, &render_adapter), + shader, per_object_buffer_batch_size: GpuArrayBuffer::::batch_size(&render_device), binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), clustered_decals_are_usable: decal::clustered::clustered_decals_are_usable( @@ -2572,13 +2570,13 @@ impl SpecializedMeshPipeline for MeshPipeline { Ok(RenderPipelineDescriptor { vertex: VertexState { - shader: MESH_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], }, fragment: Some(FragmentState { - shader: MESH_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -2876,7 +2874,7 @@ impl RenderCommand

for SetMeshViewBindGroup view_environment_map, mesh_view_bind_group, maybe_oit_layers_count_offset, - ): ROQueryItem<'w, Self::ViewQuery>, + ): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 6db72759df..0c14df01ae 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -40,7 +40,6 @@ struct DirectionalLight { num_cascades: u32, cascades_overlap_proportion: f32, depth_texture_base_index: u32, - skip: u32, }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index dcda30ee79..11e6d4d874 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -511,9 +511,6 @@ fn apply_pbr_lighting( // check if this light should be skipped, which occurs if this light does not intersect with the view // note point and spot lights aren't skippable, as the relevant lights are filtered in `assign_lights_to_clusters` let light = &view_bindings::lights.directional_lights[i]; - if (*light).skip != 0u { - continue; - } // If we're lightmapped, disable diffuse contribution from the light if // requested, to avoid double-counting light. diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 4497b567e9..01e09fe3b4 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -278,7 +278,23 @@ fn compute_specular_layer_values_for_point_light( // Representative Point Area Lights. // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 - let centerToRay = dot(light_to_frag, R) * R - light_to_frag; + var LtFdotR = dot(light_to_frag, R); + + // HACK: the following line is an amendment to fix a discontinuity when a surface + // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318 + // + // This sentence in the reference is crux of the problem: "We approximate finding the point with the + // smallest angle to the reflection ray by finding the point with the smallest distance to the ray." + // This approximation turns out to be completely wrong for points inside or near the sphere. + // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it. + // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero. + // However, this is still far from physically accurate. Deriving an exact solution would help, + // but really we should adopt a superior solution to area lighting, such as: + // Physically Based Area Lights by Michal Drobot, or + // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al. + LtFdotR = max(0.0001, LtFdotR); + + let centerToRay = LtFdotR * R - light_to_frag; let closestPoint = light_to_frag + centerToRay * saturate( light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 9224374c60..dc3ea865f2 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -1,6 +1,6 @@ use crate::NodePbr; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, prelude::Camera3d, @@ -20,6 +20,7 @@ use bevy_render::{ camera::{ExtractedCamera, TemporalJitter}, extract_component::ExtractComponent, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, prelude::Camera, render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, render_resource::{ @@ -39,38 +40,16 @@ use bevy_utils::prelude::default; use core::mem; use tracing::{error, warn}; -const PREPROCESS_DEPTH_SHADER_HANDLE: Handle = - weak_handle!("b7f2cc3d-c935-4f5c-9ae2-43d6b0d5659a"); -const SSAO_SHADER_HANDLE: Handle = weak_handle!("9ea355d7-37a2-4cc4-b4d1-5d8ab47b07f5"); -const SPATIAL_DENOISE_SHADER_HANDLE: Handle = - weak_handle!("0f2764a0-b343-471b-b7ce-ef5d636f4fc3"); -const SSAO_UTILS_SHADER_HANDLE: Handle = - weak_handle!("da53c78d-f318-473e-bdff-b388bc50ada2"); - /// Plugin for screen space ambient occlusion. pub struct ScreenSpaceAmbientOcclusionPlugin; impl Plugin for ScreenSpaceAmbientOcclusionPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - PREPROCESS_DEPTH_SHADER_HANDLE, - "preprocess_depth.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, SSAO_SHADER_HANDLE, "ssao.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - SPATIAL_DENOISE_SHADER_HANDLE, - "spatial_denoise.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SSAO_UTILS_SHADER_HANDLE, - "ssao_utils.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "ssao_utils.wgsl"); + + embedded_asset!(app, "preprocess_depth.wgsl"); + embedded_asset!(app, "ssao.wgsl"); + embedded_asset!(app, "spatial_denoise.wgsl"); app.register_type::(); @@ -321,6 +300,8 @@ struct SsaoPipelines { hilbert_index_lut: TextureView, point_clamp_sampler: Sampler, linear_clamp_sampler: Sampler, + + shader: Handle, } impl FromWorld for SsaoPipelines { @@ -431,7 +412,7 @@ impl FromWorld for SsaoPipelines { common_bind_group_layout.clone(), ], push_constant_ranges: vec![], - shader: PREPROCESS_DEPTH_SHADER_HANDLE, + shader: load_embedded_asset!(world, "preprocess_depth.wgsl"), shader_defs: Vec::new(), entry_point: "preprocess_depth".into(), zero_initialize_workgroup_memory: false, @@ -445,7 +426,7 @@ impl FromWorld for SsaoPipelines { common_bind_group_layout.clone(), ], push_constant_ranges: vec![], - shader: SPATIAL_DENOISE_SHADER_HANDLE, + shader: load_embedded_asset!(world, "spatial_denoise.wgsl"), shader_defs: Vec::new(), entry_point: "spatial_denoise".into(), zero_initialize_workgroup_memory: false, @@ -463,6 +444,8 @@ impl FromWorld for SsaoPipelines { hilbert_index_lut, point_clamp_sampler, linear_clamp_sampler, + + shader: load_embedded_asset!(world, "ssao.wgsl"), } } } @@ -498,7 +481,7 @@ impl SpecializedComputePipeline for SsaoPipelines { self.common_bind_group_layout.clone(), ], push_constant_ranges: vec![], - shader: SSAO_SHADER_HANDLE, + shader: self.shader.clone(), 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 e4cc850d81..22d5acd1e9 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -1,7 +1,7 @@ //! Screen space reflections implemented via raymarching. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::{ graph::{Core3d, Node3d}, @@ -23,7 +23,6 @@ use bevy_ecs::{ }; use bevy_image::BevyDefault as _; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::render_graph::RenderGraph; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, @@ -39,6 +38,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSystems, }; +use bevy_render::{load_shader_library, render_graph::RenderGraph}; use bevy_utils::{once, prelude::default}; use tracing::info; @@ -49,9 +49,6 @@ use crate::{ ViewLightsUniformOffset, }; -const SSR_SHADER_HANDLE: Handle = weak_handle!("0b559df2-0d61-4f53-bf62-aea16cf32787"); -const RAYMARCH_SHADER_HANDLE: Handle = weak_handle!("798cc6fc-6072-4b6c-ab4f-83905fa4a19e"); - /// Enables screen-space reflections for a camera. /// /// Screen-space reflections are currently only supported with deferred rendering. @@ -158,6 +155,7 @@ pub struct ScreenSpaceReflectionsPipeline { depth_nearest_sampler: Sampler, bind_group_layout: BindGroupLayout, binding_arrays_are_usable: bool, + shader: Handle, } /// A GPU buffer that stores the screen space reflection settings for each view. @@ -179,13 +177,8 @@ pub struct ScreenSpaceReflectionsPipelineKey { impl Plugin for ScreenSpaceReflectionsPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, SSR_SHADER_HANDLE, "ssr.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - RAYMARCH_SHADER_HANDLE, - "raymarch.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "ssr.wgsl"); + load_shader_library!(app, "raymarch.wgsl"); app.register_type::() .add_plugins(ExtractComponentPlugin::::default()); @@ -287,7 +280,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { view_environment_map_offset, view_bind_group, ssr_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // Grab the render pipeline. @@ -404,6 +397,9 @@ impl FromWorld for ScreenSpaceReflectionsPipeline { depth_nearest_sampler, bind_group_layout, binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), + // Even though ssr was loaded using load_shader_library, we can still access it like a + // normal embedded asset (so we can use it as both a library or a kernel). + shader: load_embedded_asset!(world, "ssr.wgsl"), } } } @@ -502,7 +498,7 @@ impl ExtractComponent for ScreenSpaceReflections { type Out = ScreenSpaceReflectionsUniform; - fn extract_component(settings: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { once!(info!( "Disabling screen-space reflections on this platform because depth textures \ @@ -542,7 +538,7 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { layout: vec![mesh_view_layout.clone(), self.bind_group_layout.clone()], vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), fragment: Some(FragmentState { - shader: SSR_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index 4af1bbb421..efc3c5370a 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -30,7 +30,7 @@ //! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, Assets, Handle}; +use bevy_asset::{embedded_asset, Assets, Handle}; use bevy_color::Color; use bevy_core_pipeline::core_3d::{ graph::{Core3d, Node3d}, @@ -48,7 +48,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ mesh::{Mesh, Meshable}, render_graph::{RenderGraphApp, ViewNodeRunner}, - render_resource::{Shader, SpecializedRenderPipelines}, + render_resource::SpecializedRenderPipelines, sync_component::SyncComponentPlugin, view::Visibility, ExtractSchedule, Render, RenderApp, RenderSystems, @@ -56,7 +56,6 @@ use bevy_render::{ use bevy_transform::components::Transform; use render::{ VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer, CUBE_MESH, PLANE_MESH, - VOLUMETRIC_FOG_HANDLE, }; use crate::graph::NodePbr; @@ -189,12 +188,7 @@ pub struct FogVolume { impl Plugin for VolumetricFogPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - VOLUMETRIC_FOG_HANDLE, - "volumetric_fog.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "volumetric_fog.wgsl"); let mut meshes = app.world_mut().resource_mut::>(); meshes.insert(&PLANE_MESH, Plane3d::new(Vec3::Z, Vec2::ONE).mesh().into()); diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 07012a72e2..cf2989a980 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -2,7 +2,7 @@ use core::array; -use bevy_asset::{weak_handle, AssetId, Handle}; +use bevy_asset::{load_embedded_asset, weak_handle, AssetId, Handle}; use bevy_color::ColorToComponents as _; use bevy_core_pipeline::{ core_3d::Camera3d, @@ -77,10 +77,6 @@ bitflags! { } } -/// The volumetric fog shader. -pub const VOLUMETRIC_FOG_HANDLE: Handle = - weak_handle!("481f474c-2024-44bb-8f79-f7c05ced95ea"); - /// The plane mesh, which is used to render a fog volume that the camera is /// inside. /// @@ -121,6 +117,9 @@ pub struct VolumetricFogPipeline { /// /// Since there aren't too many of these, we precompile them all. volumetric_view_bind_group_layouts: [BindGroupLayout; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT], + + // The shader asset handle. + shader: Handle, } /// The two render pipelines that we use for fog volumes: one for when a 3D @@ -266,6 +265,7 @@ impl FromWorld for VolumetricFogPipeline { VolumetricFogPipeline { mesh_view_layouts: mesh_view_layouts.clone(), volumetric_view_bind_group_layouts: bind_group_layouts, + shader: load_embedded_asset!(world, "volumetric_fog.wgsl"), } } } @@ -347,7 +347,7 @@ impl ViewNode for VolumetricFogNode { view_ssr_offset, msaa, view_environment_map_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -564,7 +564,7 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { layout: vec![mesh_view_layout.clone(), volumetric_view_bind_group_layout], push_constant_ranges: vec![], vertex: VertexState { - shader: VOLUMETRIC_FOG_HANDLE, + shader: self.shader.clone(), shader_defs: shader_defs.clone(), entry_point: "vertex".into(), buffers: vec![vertex_format], @@ -576,7 +576,7 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { depth_stencil: None, multisample: MultisampleState::default(), fragment: Some(FragmentState { - shader: VOLUMETRIC_FOG_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -700,7 +700,7 @@ pub fn prepare_volumetric_fog_uniforms( // Do this up front to avoid O(n^2) matrix inversion. local_from_world_matrices.clear(); for (_, _, fog_transform) in fog_volumes.iter() { - local_from_world_matrices.push(fog_transform.compute_matrix().inverse()); + local_from_world_matrices.push(fog_transform.to_matrix().inverse()); } let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); @@ -712,7 +712,7 @@ pub fn prepare_volumetric_fog_uniforms( }; for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let mut view_fog_volumes = vec![]; diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 7b748c2535..e42e1309ec 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ - load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, + embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; @@ -56,9 +56,6 @@ use bevy_render::{ use core::{hash::Hash, ops::Range}; use tracing::error; -pub const WIREFRAME_SHADER_HANDLE: Handle = - weak_handle!("2646a633-f8e3-4380-87ae-b44d881abbce"); - /// A [`Plugin`] that draws wireframes. /// /// Wireframes currently do not work when using webgl or webgpu. @@ -83,12 +80,7 @@ impl WireframePlugin { impl Plugin for WireframePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - WIREFRAME_SHADER_HANDLE, - "render/wireframe.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "render/wireframe.wgsl"); app.add_plugins(( BinnedRenderPhasePlugin::::new(self.debug_flags), @@ -341,7 +333,7 @@ impl FromWorld for Wireframe3dPipeline { fn from_world(render_world: &mut World) -> Self { Wireframe3dPipeline { mesh_pipeline: render_world.resource::().clone(), - shader: WIREFRAME_SHADER_HANDLE, + shader: load_embedded_asset!(render_world, "render/wireframe.wgsl"), } } } @@ -382,7 +374,7 @@ impl ViewNode for Wireframe3dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(wireframe_phase) = world.get_resource::>() diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index f02e5237aa..ac18b1dd89 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_picking" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides screen picking functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" @@ -13,20 +13,20 @@ bevy_mesh_picking_backend = ["dep:bevy_mesh", "dep:crossbeam-channel"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 3758816ac9..28693314d9 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -55,7 +55,7 @@ pub mod prelude { /// Note that systems reading these events in [`PreUpdate`](bevy_app::PreUpdate) will not report ordering /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered /// against [`PickingSystems::Backend`](crate::PickingSystems::Backend), or better, avoid reading `PointerHits` in `PreUpdate`. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct PointerHits { /// The pointer associated with this hit test. @@ -84,7 +84,7 @@ pub struct PointerHits { } impl PointerHits { - #[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + /// Construct [`PointerHits`]. pub fn new(pointer: prelude::PointerId, picks: Vec<(Entity, HitData)>, order: f32) -> Self { Self { pointer, @@ -114,7 +114,7 @@ pub struct HitData { } impl HitData { - #[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + /// Construct a [`HitData`]. pub fn new(camera: Entity, depth: f32, position: Option, normal: Option) -> Self { Self { camera, diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 29405099f6..a7a3979c59 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -11,7 +11,7 @@ //! # use bevy_picking::prelude::*; //! # let mut world = World::default(); //! world.spawn_empty() -//! .observe(|trigger: Trigger>| { +//! .observe(|trigger: On>| { //! println!("I am being hovered over"); //! }); //! ``` @@ -31,7 +31,7 @@ //! //! The events this module defines fall into a few broad categories: //! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. -//! + Clicking and pressing: [`Pressed`], [`Released`], and [`Click`]. +//! + Clicking and pressing: [`Press`], [`Release`], and [`Click`]. //! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. //! //! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains @@ -59,11 +59,10 @@ use crate::{ /// /// The documentation for the [`pointer_events`] explains the events this module exposes and /// the order in which they fire. -#[derive(Clone, PartialEq, Debug, Reflect, Component)] +#[derive(Event, BufferedEvent, EntityEvent, Clone, PartialEq, Debug, Reflect, Component)] +#[entity_event(traversal = PointerTraversal, auto_propagate)] #[reflect(Component, Debug, Clone)] pub struct Pointer { - /// The original target of this picking event, before bubbling - pub target: Entity, /// The pointer that triggered this event pub pointer_id: PointerId, /// The location of the pointer during this event @@ -87,7 +86,7 @@ impl Traversal> for PointerTraversal where E: Debug + Clone + Reflect, { - fn traverse(item: Self::Item<'_>, pointer: &Pointer) -> Option { + fn traverse(item: Self::Item<'_, '_>, pointer: &Pointer) -> Option { let PointerTraversalItem { child_of, window } = item; // Send event to parent, if it has one. @@ -106,15 +105,6 @@ where } } -impl Event for Pointer -where - E: Debug + Clone + Reflect, -{ - type Traversal = PointerTraversal; - - const AUTO_PROPAGATE: bool = true; -} - impl core::fmt::Display for Pointer { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( @@ -134,9 +124,8 @@ impl core::ops::Deref for Pointer { impl Pointer { /// Construct a new `Pointer` event. - pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { + pub fn new(id: PointerId, location: Location, event: E) -> Self { Self { - target, pointer_id: id, pointer_location: location, event, @@ -171,7 +160,7 @@ pub struct Out { /// Fires when a pointer button is pressed over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Pressed { +pub struct Press { /// Pointer button pressed to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -181,7 +170,7 @@ pub struct Pressed { /// Fires when a pointer button is released over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Released { +pub struct Release { /// Pointer button lifted to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -208,6 +197,11 @@ pub struct Move { /// Information about the picking intersection. pub hit: HitData, /// The change in position since the last move event. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub delta: Vec2, } @@ -228,8 +222,18 @@ pub struct Drag { /// Pointer button pressed and moved to trigger this event. pub button: PointerButton, /// The total distance vector of a drag, measured from drag start to the current position. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub distance: Vec2, /// The change in position since the last drag event. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub delta: Vec2, } @@ -240,6 +244,11 @@ pub struct DragEnd { /// Pointer button pressed, moved, and released to trigger this event. pub button: PointerButton, /// The vector of drag movement measured from start to final pointer position. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub distance: Vec2, } @@ -296,8 +305,20 @@ pub struct DragDrop { #[reflect(Clone, PartialEq)] pub struct DragEntry { /// The position of the pointer at drag start. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using [`Camera::viewport_to_world`](bevy_render::camera::Camera::viewport_to_world) or + /// [`Camera::viewport_to_world_2d`](bevy_render::camera::Camera::viewport_to_world_2d) to + /// convert from screen-space to world-space. pub start_pos: Vec2, /// The latest position of the pointer during this drag, used to compute deltas. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using [`Camera::viewport_to_world`](bevy_render::camera::Camera::viewport_to_world) or + /// [`Camera::viewport_to_world_2d`](bevy_render::camera::Camera::viewport_to_world_2d) to + /// convert from screen-space to world-space. pub latest_pos: Vec2, } @@ -368,7 +389,7 @@ impl PointerState { pub struct PickingEventWriters<'w> { cancel_events: EventWriter<'w, Pointer>, click_events: EventWriter<'w, Pointer>, - pressed_events: EventWriter<'w, Pointer>, + pressed_events: EventWriter<'w, Pointer>, drag_drop_events: EventWriter<'w, Pointer>, drag_end_events: EventWriter<'w, Pointer>, drag_enter_events: EventWriter<'w, Pointer>, @@ -380,7 +401,7 @@ pub struct PickingEventWriters<'w> { move_events: EventWriter<'w, Pointer>, out_events: EventWriter<'w, Pointer>, over_events: EventWriter<'w, Pointer>, - released_events: EventWriter<'w, Pointer>, + released_events: EventWriter<'w, Pointer>, } /// Dispatches interaction events to the target entities. @@ -390,7 +411,7 @@ pub struct PickingEventWriters<'w> { /// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. -/// + For each button press: [`Pressed`] or [`Click`] → [`Released`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each button press: [`Press`] or [`Click`] → [`Release`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. /// + For each pointer cancellation: [`Cancel`]. /// /// Additionally, across multiple frames, the following are also strictly @@ -398,7 +419,7 @@ pub struct PickingEventWriters<'w> { /// + When a pointer moves over the target: /// [`Over`], [`Move`], [`Out`]. /// + When a pointer presses buttons on the target: -/// [`Pressed`], [`Click`], [`Released`]. +/// [`Press`], [`Click`], [`Release`]. /// + When a pointer drags the target: /// [`DragStart`], [`Drag`], [`DragEnd`]. /// + When a pointer drags something over the target: @@ -420,7 +441,7 @@ pub struct PickingEventWriters<'w> { /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. /// -/// Both [`Click`] and [`Released`] target the entity hovered in the *previous frame*, +/// Both [`Click`] and [`Release`] 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 sequentially after an [`Out`] event (but always on the same frame @@ -473,12 +494,7 @@ pub fn pointer_events( }; // Always send Out events - let out_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Out { hit: hit.clone() }, - ); + let out_event = Pointer::new(pointer_id, location.clone(), Out { hit: hit.clone() }); commands.trigger_targets(out_event.clone(), hovered_entity); event_writers.out_events.write(out_event); @@ -490,7 +506,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragLeave { button, dragged: *drag_target, @@ -527,16 +542,11 @@ pub fn pointer_events( for button in PointerButton::iter() { let state = pointer_state.get_mut(pointer_id, button); - for drag_target in state - .dragging - .keys() - .filter(|&&drag_target| hovered_entity != drag_target) - { + for drag_target in state.dragging.keys() { state.dragging_over.insert(hovered_entity, hit.clone()); let drag_enter_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragEnter { button, dragged: *drag_target, @@ -549,12 +559,7 @@ pub fn pointer_events( } // Always send Over events - let over_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Over { hit: hit.clone() }, - ); + let over_event = Pointer::new(pointer_id, location.clone(), Over { hit: hit.clone() }); commands.trigger_targets(over_event.clone(), hovered_entity); event_writers.over_events.write(over_event); } @@ -580,8 +585,7 @@ pub fn pointer_events( let pressed_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -608,7 +612,6 @@ pub fn pointer_events( let click_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Click { button, hit: hit.clone(), @@ -618,12 +621,11 @@ pub fn pointer_events( commands.trigger_targets(click_event.clone(), hovered_entity); event_writers.click_events.write(click_event); } - // Always send the Released event + // Always send the Release event let released_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Released { + Release { button, hit: hit.clone(), }, @@ -639,7 +641,6 @@ pub fn pointer_events( let drag_drop_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragDrop { button, dropped: drag_target, @@ -653,7 +654,6 @@ pub fn pointer_events( let drag_end_event = Pointer::new( pointer_id, location.clone(), - drag_target, DragEnd { button, distance: drag.latest_pos - drag.start_pos, @@ -666,7 +666,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragLeave { button, dragged: drag_target, @@ -707,7 +706,6 @@ pub fn pointer_events( let drag_start_event = Pointer::new( pointer_id, location.clone(), - *press_target, DragStart { button, hit: hit.clone(), @@ -726,7 +724,6 @@ pub fn pointer_events( let drag_event = Pointer::new( pointer_id, location.clone(), - *drag_target, Drag { button, distance: location.position - drag.start_pos, @@ -749,7 +746,6 @@ pub fn pointer_events( let drag_over_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragOver { button, dragged: *drag_target, @@ -771,7 +767,6 @@ pub fn pointer_events( let move_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Move { hit: hit.clone(), delta, @@ -791,7 +786,6 @@ pub fn pointer_events( let scroll_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Scroll { unit, x, @@ -811,8 +805,7 @@ pub fn pointer_events( .iter() .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) { - let cancel_event = - Pointer::new(pointer_id, location.clone(), hovered_entity, Cancel { hit }); + let cancel_event = Pointer::new(pointer_id, location.clone(), Cancel { hit }); commands.trigger_targets(cancel_event.clone(), hovered_entity); event_writers.cancel_events.write(cancel_event); } diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 6347568c02..dbb6ee942e 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -14,7 +14,7 @@ use crate::{ }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; @@ -208,18 +208,6 @@ pub fn update_interactions( mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, mut interact: Query<&mut PickingInteraction>, ) { - // Clear all previous hover data from pointers and entities - for (pointer, _, mut pointer_interaction) in &mut pointers { - pointer_interaction.sorted_entities.clear(); - if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) { - for entity in previously_hovered_entities.keys() { - if let Ok(mut interaction) = interact.get_mut(*entity) { - *interaction = PickingInteraction::None; - } - } - } - } - // Create a map to hold the aggregated interaction for each entity. This is needed because we // need to be able to insert the interaction component on entities if they do not exist. To do // so we need to know the final aggregated interaction state to avoid the scenario where we set @@ -239,13 +227,29 @@ pub fn update_interactions( } // Take the aggregated entity states and update or insert the component if missing. - for (hovered_entity, new_interaction) in new_interaction_state.drain() { + for (&hovered_entity, &new_interaction) in new_interaction_state.iter() { if let Ok(mut interaction) = interact.get_mut(hovered_entity) { - *interaction = new_interaction; + interaction.set_if_neq(new_interaction); } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) { entity_commands.try_insert(new_interaction); } } + + // Clear all previous hover data from pointers that are no longer hovering any entities. + // We do this last to preserve change detection for picking interactions. + for (pointer, _, _) in &mut pointers { + let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else { + continue; + }; + + for entity in previously_hovered_entities.keys() { + if !new_interaction_state.contains_key(entity) { + if let Ok(mut interaction) = interact.get_mut(*entity) { + interaction.set_if_neq(PickingInteraction::None); + } + } + } + } } /// Merge the interaction state of this entity into the aggregated map. @@ -275,3 +279,285 @@ fn merge_interaction_states( new_interaction_state.insert(*hovered_entity, new_interaction); } } + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// enters or leaves an entity. Users should insert this component on an entity to indicate interest +/// in knowing about hover state changes. +/// +/// The component's boolean value will be `true` whenever the pointer is currently directly hovering +/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`] +/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which +/// applies to the element and all of its descendants. +/// +/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves +/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the +/// [`HoverMap`] resource, which is updated every frame. +/// +/// Typically, a simple hoverable entity or widget will have this component added to it. More +/// complex widgets can have this component added to each hoverable part. +/// +/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`Hovered`] component inserted. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct Hovered(pub bool); + +impl Hovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// is directly hovering over an entity. Users should insert this component on an entity to indicate +/// interest in knowing about hover state changes. +/// +/// This is similar to [`Hovered`] component, except that it does not include descendants in the +/// hover state. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct DirectlyHovered(pub bool); + +impl DirectlyHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// Uses [`HoverMap`] changes to update [`Hovered`] components. +pub fn update_is_hovered( + hover_map: Option>, + mut hovers: Query<(Entity, &Hovered)>, + parent_query: Query<&ChildOf>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + // Algorithm: for each entity having a `Hovered` component, we want to know if the current + // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather + // than doing an expensive breadth-first traversal of children, instead start with the hovermap + // entry and search upwards. We can make this even cheaper by building a set of ancestors for + // the hovermap entry, and then testing each `Hovered` entity against that set. + + // A set which contains the hovered for the current pointer entity and its ancestors. The + // capacity is based on the likely tree depth of the hierarchy, which is typically greater for + // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound + // for most use cases. + let mut hover_ancestors = EntityHashSet::with_capacity(32); + if let Some(map) = hover_map.get(&PointerId::Mouse) { + for hovered_entity in map.keys() { + hover_ancestors.insert(*hovered_entity); + hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); + } + } + + // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. + for (entity, hoverable) in hovers.iter_mut() { + let is_hovering = hover_ancestors.contains(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(Hovered(is_hovering)); + } + } +} + +/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components. +pub fn update_is_directly_hovered( + hover_map: Option>, + hovers: Query<(Entity, &DirectlyHovered)>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + if let Some(map) = hover_map.get(&PointerId::Mouse) { + // It's hovering if it's in the HoverMap. + for (entity, hoverable) in hovers.iter() { + let is_hovering = map.contains_key(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(DirectlyHovered(is_hovering)); + } + } + } else { + // No hovered entity, reset all hovers. + for (entity, hoverable) in hovers.iter() { + if hoverable.0 { + commands.entity(entity).insert(DirectlyHovered(false)); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_render::camera::Camera; + + use super::*; + + #[test] + fn update_is_hovered_memoized() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_hovered).is_ok()); + + // Check to insure that the hovered entity has the Hovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_self() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_entity = world.spawn(DirectlyHovered(false)).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_entity, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the hovered entity has the DirectlyHovered component set to true + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_child() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world + .spawn(DirectlyHovered(false)) + .add_child(hovered_child) + .id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the DirectlyHovered component is still false + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 53387e84c8..74a765fbcd 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -17,7 +17,7 @@ //! # struct MyComponent; //! # let mut world = World::new(); //! world.spawn(MyComponent) -//! .observe(|mut trigger: Trigger>| { +//! .observe(|mut trigger: On>| { //! println!("I was just clicked!"); //! // Get the underlying pointer event data //! let click_event: &Pointer = trigger.event(); @@ -39,7 +39,7 @@ //! //! 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. +//! [`On::propagate`](bevy_ecs::observer::On::propagate). See [`Observer`] for details. //! //! This allows you to run callbacks when any children of an entity are interacted with, and leads //! to succinct, expressive code: @@ -48,22 +48,22 @@ //! # use bevy_ecs::prelude::*; //! # use bevy_transform::prelude::*; //! # use bevy_picking::prelude::*; -//! # #[derive(Event)] +//! # #[derive(Event, BufferedEvent)] //! # struct Greeting; //! fn setup(mut commands: Commands) { //! commands.spawn(Transform::default()) -//! // Spawn your entity here, e.g. a Mesh. +//! // Spawn your entity here, e.g. a `Mesh3d`. //! // When dragged, mutate the `Transform` component on the dragged target entity: -//! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { +//! .observe(|trigger: On>, mut transforms: Query<&mut Transform>| { //! let mut transform = transforms.get_mut(trigger.target()).unwrap(); //! let drag = trigger.event(); //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) -//! .observe(|trigger: Trigger>, mut commands: Commands| { +//! .observe(|trigger: On>, mut commands: Commands| { //! println!("Entity {} goes BOOM!", trigger.target()); //! commands.entity(trigger.target()).despawn(); //! }) -//! .observe(|trigger: Trigger>, mut events: EventWriter| { +//! .observe(|trigger: On>, mut events: EventWriter| { //! events.write(Greeting); //! }); //! } @@ -170,6 +170,7 @@ pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +use hover::{update_is_directly_hovered, update_is_hovered}; /// The picking prelude. /// @@ -285,7 +286,7 @@ pub type PickSet = PickingSystems; /// /// Note: for any of these plugins to work, they require a picking backend to be active, /// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] -/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. +/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::On`]s. #[derive(Default)] pub struct DefaultPickingPlugins; @@ -392,6 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -414,7 +416,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -425,11 +427,16 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_systems( PreUpdate, - (generate_hovermap, update_interactions, pointer_events) + ( + generate_hovermap, + update_interactions, + (update_is_hovered, update_is_directly_hovered), + pointer_events, + ) .chain() .in_set(PickingSystems::Hover), ); diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index fe08976eb8..8e6a16690c 100644 --- a/crates/bevy_picking/src/mesh_picking/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/mod.rs @@ -1,7 +1,7 @@ //! A [mesh ray casting](ray_cast) backend for [`bevy_picking`](crate). //! -//! By default, all meshes are pickable. Picking can be disabled for individual entities -//! by adding [`Pickable::IGNORE`]. +//! By default, all meshes that have [`bevy_asset::RenderAssetUsages::MAIN_WORLD`] are pickable. +//! Picking can be disabled for individual entities by adding [`Pickable::IGNORE`]. //! //! To make mesh picking entirely opt-in, set [`MeshPickingSettings::require_markers`] //! to `true` and add [`MeshPickingCamera`] and [`Pickable`] components to the desired camera and diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 9988a96e19..5ac2d89887 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -317,7 +317,7 @@ mod tests { #[test] fn ray_mesh_intersection_simple() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = None; @@ -338,7 +338,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -359,7 +359,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -381,7 +381,7 @@ mod tests { #[test] fn ray_mesh_intersection_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -403,7 +403,7 @@ mod tests { #[test] fn ray_mesh_intersection_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = None; @@ -424,7 +424,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -445,7 +445,7 @@ mod tests { #[test] fn ray_mesh_intersection_not_enough_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0]); @@ -466,7 +466,7 @@ mod tests { #[test] fn ray_mesh_intersection_bad_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 3]); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index c1f465b96a..e42dc160e2 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -233,7 +233,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { if let Some(distance) = ray_aabb_intersection_3d( ray, &Aabb3d::new(aabb.center, aabb.half_extents), - &transform.compute_matrix(), + &transform.to_matrix(), ) { aabb_hits_tx.send((FloatOrd(distance), entity)).ok(); } @@ -287,7 +287,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { // Perform the actual ray cast. let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.compute_matrix(); + let transform = transform.to_matrix(); let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); if let Some(intersection) = intersection { diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index e180a9c1be..0406cb61f5 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -205,8 +205,8 @@ impl PointerLocation { /// - a pointer is not associated with a [`Camera`] because multiple cameras can target the same /// render target. It is up to picking backends to associate a Pointer's `Location` with a /// specific `Camera`, if any. -#[derive(Debug, Clone, Component, Reflect, PartialEq)] -#[reflect(Component, Debug, PartialEq, Clone)] +#[derive(Debug, Clone, Reflect, PartialEq)] +#[reflect(Debug, PartialEq, Clone)] pub struct Location { /// The [`NormalizedRenderTarget`] associated with the pointer, usually a window. pub target: NormalizedRenderTarget, @@ -269,7 +269,7 @@ pub enum PointerAction { } /// An input event effecting a pointer. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Clone)] pub struct PointerInput { /// The id of the pointer. diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index 44c680394d..614ff15fce 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_platform" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides common platform agnostic APIs, as well as platform-specific features for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] diff --git a/crates/bevy_platform/src/cell/mod.rs b/crates/bevy_platform/src/cell/mod.rs new file mode 100644 index 0000000000..04f8bf6572 --- /dev/null +++ b/crates/bevy_platform/src/cell/mod.rs @@ -0,0 +1,9 @@ +//! Provides cell primitives. +//! +//! This is a drop-in replacement for `std::cell::SyncCell`/`std::cell::SyncUnsafeCell`. + +mod sync_cell; +mod sync_unsafe_cell; + +pub use sync_cell::SyncCell; +pub use sync_unsafe_cell::SyncUnsafeCell; diff --git a/crates/bevy_utils/src/synccell.rs b/crates/bevy_platform/src/cell/sync_cell.rs similarity index 100% rename from crates/bevy_utils/src/synccell.rs rename to crates/bevy_platform/src/cell/sync_cell.rs diff --git a/crates/bevy_utils/src/syncunsafecell.rs b/crates/bevy_platform/src/cell/sync_unsafe_cell.rs similarity index 98% rename from crates/bevy_utils/src/syncunsafecell.rs rename to crates/bevy_platform/src/cell/sync_unsafe_cell.rs index 104256969d..bb67557ec2 100644 --- a/crates/bevy_utils/src/syncunsafecell.rs +++ b/crates/bevy_platform/src/cell/sync_unsafe_cell.rs @@ -94,7 +94,7 @@ impl SyncUnsafeCell<[T]> { /// # Examples /// /// ``` - /// # use bevy_utils::syncunsafecell::SyncUnsafeCell; + /// # use bevy_platform::cell::SyncUnsafeCell; /// /// let slice: &mut [i32] = &mut [1, 2, 3]; /// let cell_slice: &SyncUnsafeCell<[i32]> = SyncUnsafeCell::from_mut(slice); diff --git a/crates/bevy_platform/src/lib.rs b/crates/bevy_platform/src/lib.rs index 668442f299..0dac76b011 100644 --- a/crates/bevy_platform/src/lib.rs +++ b/crates/bevy_platform/src/lib.rs @@ -1,13 +1,13 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] //! Platform compatibility support for first-party [Bevy] engine crates. //! -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ cfg::std! { extern crate std; @@ -19,6 +19,7 @@ cfg::alloc! { pub mod collections; } +pub mod cell; pub mod cfg; pub mod hash; pub mod sync; diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index 0f56880bd4..07c6eeae68 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_ptr" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Utilities for working with untyped pointers in a more safe way" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "no_std"] diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 1580f3f926..15a193d737 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -3,8 +3,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![expect(unsafe_code, reason = "Raw pointers are inherently unsafe.")] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] use core::{ @@ -27,7 +27,9 @@ pub struct Unaligned; /// Trait that is only implemented for [`Aligned`] and [`Unaligned`] to work around the lack of ability /// to have const generics of an enum. pub trait IsAligned: sealed::Sealed {} + impl IsAligned for Aligned {} + impl IsAligned for Unaligned {} mod sealed { diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index bb72226ab8..087cdb44db 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_reflect" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Dynamically interact with rust types" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -54,7 +54,6 @@ wgpu-types = ["dep:wgpu-types"] ## on `no_std` targets, but provides access to certain additional features on ## supported platforms. std = [ - "bevy_utils/std", "erased-serde/std", "downcast-rs/std", "serde/std", @@ -67,10 +66,7 @@ std = [ ## `critical-section` provides the building blocks for synchronization primitives ## on all platforms, including `no_std`. -critical-section = [ - "bevy_platform/critical-section", - "bevy_utils/critical-section", -] +critical-section = ["bevy_platform/critical-section"] ## Enables use of browser APIs. ## Note this is currently only applicable on `wasm32` architectures. @@ -78,12 +74,10 @@ web = ["bevy_platform/web", "uuid?/js"] [dependencies] # bevy -bevy_reflect_derive = { path = "derive", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ - "alloc", -] } -bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect_derive = { path = "derive", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_ptr = { path = "../bevy_ptr", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", "serialize", ] } @@ -120,10 +114,10 @@ wgpu-types = { version = "24", features = [ ], optional = true, default-features = false } [dev-dependencies] -ron = "0.8.0" +ron = "0.10" rmp-serde = "1.1" bincode = { version = "2.0", features = ["serde"] } -serde_json = "1.0" +serde_json = "1.0.140" serde = { version = "1", features = ["derive"] } static_assertions = "1.1.0" diff --git a/crates/bevy_reflect/compile_fail/Cargo.toml b/crates/bevy_reflect/compile_fail/Cargo.toml index 178711c5d0..e3cb14ec2d 100644 --- a/crates/bevy_reflect/compile_fail/Cargo.toml +++ b/crates/bevy_reflect/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_reflect_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's reflection system" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" publish = false diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index ad6ec8cd2f..032046ae2f 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_reflect_derive" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_reflect" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -19,7 +19,7 @@ documentation = [] functions = [] [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 8be0110a3e..df9580b820 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -167,6 +167,7 @@ pub struct DynamicArray { } impl DynamicArray { + /// Creates a new [`DynamicArray`]. #[inline] pub fn new(values: Box<[Box]>) -> Self { Self { @@ -186,8 +187,7 @@ impl DynamicArray { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Array(_)), - "expected TypeInfo::Array but received: {:?}", - represented_type + "expected TypeInfo::Array but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs index a6edefab25..4d8154fad3 100644 --- a/crates/bevy_reflect/src/attributes.rs +++ b/crates/bevy_reflect/src/attributes.rs @@ -1,3 +1,5 @@ +//! Types and functions for creating, manipulating and querying [`CustomAttributes`]. + use crate::Reflect; use alloc::boxed::Box; use bevy_utils::TypeIdMap; @@ -98,16 +100,19 @@ struct CustomAttribute { } impl CustomAttribute { + /// Creates a new [`CustomAttribute`] containing `value`. pub fn new(value: T) -> Self { Self { value: Box::new(value), } } + /// Returns a reference to the attribute's value if it is of type `T`, or [`None`] if not. pub fn value(&self) -> Option<&T> { self.value.downcast_ref() } + /// Returns a reference to the attribute's value as a [`Reflect`] trait object. pub fn reflect_value(&self) -> &dyn Reflect { &*self.value } @@ -213,7 +218,7 @@ mod tests { fn should_debug_custom_attributes() { let attributes = CustomAttributes::default().with_attribute("My awesome custom attribute!"); - let debug = format!("{:?}", attributes); + let debug = format!("{attributes:?}"); assert_eq!(r#"{"My awesome custom attribute!"}"#, debug); @@ -224,7 +229,7 @@ mod tests { let attributes = CustomAttributes::default().with_attribute(Foo { value: 42 }); - let debug = format!("{:?}", attributes); + let debug = format!("{attributes:?}"); assert_eq!( r#"{bevy_reflect::attributes::tests::Foo { value: 42 }}"#, diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 42c20e1956..2835306b22 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -13,9 +13,12 @@ use derive_more::derive::From; /// A dynamic representation of an enum variant. #[derive(Debug, Default, From)] pub enum DynamicVariant { + /// A unit variant. #[default] Unit, + /// A tuple variant. Tuple(DynamicTuple), + /// A struct variant. Struct(DynamicStruct), } @@ -114,8 +117,7 @@ impl DynamicEnum { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Enum(_)), - "expected TypeInfo::Enum but received: {:?}", - represented_type + "expected TypeInfo::Enum but received: {represented_type:?}", ); } diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index 126c407f23..32e4b96124 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -263,6 +263,7 @@ pub struct VariantFieldIter<'a> { } impl<'a> VariantFieldIter<'a> { + /// Creates a new [`VariantFieldIter`]. pub fn new(container: &'a dyn Enum) -> Self { Self { container, @@ -295,12 +296,16 @@ impl<'a> Iterator for VariantFieldIter<'a> { impl<'a> ExactSizeIterator for VariantFieldIter<'a> {} +/// A field in the current enum variant. pub enum VariantField<'a> { + /// The name and value of a field in a struct variant. Struct(&'a str, &'a dyn PartialReflect), + /// The value of a field in a tuple variant. Tuple(&'a dyn PartialReflect), } impl<'a> VariantField<'a> { + /// Returns the name of a struct variant field, or [`None`] for a tuple variant field. pub fn name(&self) -> Option<&'a str> { if let Self::Struct(name, ..) = self { Some(*name) @@ -309,6 +314,7 @@ impl<'a> VariantField<'a> { } } + /// Gets a reference to the value of this field. pub fn value(&self) -> &'a dyn PartialReflect { match *self { Self::Struct(_, value) | Self::Tuple(value) => value, diff --git a/crates/bevy_reflect/src/enums/variants.rs b/crates/bevy_reflect/src/enums/variants.rs index 55ccb8efb1..d4fcc2845a 100644 --- a/crates/bevy_reflect/src/enums/variants.rs +++ b/crates/bevy_reflect/src/enums/variants.rs @@ -47,7 +47,9 @@ pub enum VariantInfoError { /// [type]: VariantType #[error("variant type mismatch: expected {expected:?}, received {received:?}")] TypeMismatch { + /// Expected variant type. expected: VariantType, + /// Received variant type. received: VariantType, }, } @@ -84,6 +86,7 @@ pub enum VariantInfo { } impl VariantInfo { + /// The name of the enum variant. pub fn name(&self) -> &'static str { match self { Self::Struct(info) => info.name(), diff --git a/crates/bevy_reflect/src/error.rs b/crates/bevy_reflect/src/error.rs index e783a33775..d8bb8a9e14 100644 --- a/crates/bevy_reflect/src/error.rs +++ b/crates/bevy_reflect/src/error.rs @@ -11,14 +11,20 @@ pub enum ReflectCloneError { /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`PartialReflect::reflect_clone` not implemented for `{type_path}`")] - NotImplemented { type_path: Cow<'static, str> }, + NotImplemented { + /// The fully qualified path of the type that [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone) is not implemented for. + type_path: Cow<'static, str>, + }, /// The type cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// This type should be returned when a type is intentionally opting out of reflection cloning. /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`{type_path}` cannot be made cloneable for `PartialReflect::reflect_clone`")] - NotCloneable { type_path: Cow<'static, str> }, + NotCloneable { + /// The fully qualified path of the type that cannot be cloned via [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone). + type_path: Cow<'static, str>, + }, /// The field cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// When [deriving `Reflect`], this usually means that a field marked with `#[reflect(ignore)]` @@ -33,8 +39,11 @@ pub enum ReflectCloneError { full_path(.field, .variant.as_deref(), .container_type_path) )] FieldNotCloneable { + /// Struct field or enum variant field which cannot be cloned. field: FieldId, + /// Variant this field is part of if the container is an enum, otherwise [`None`]. variant: Option>, + /// Fully qualified path of the type containing this field. container_type_path: Cow<'static, str>, }, /// Could not downcast to the expected type. @@ -44,7 +53,9 @@ pub enum ReflectCloneError { /// [`Reflect`]: crate::Reflect #[error("expected downcast to `{expected}`, but received `{received}`")] FailedDowncast { + /// The fully qualified path of the type that was expected. expected: Cow<'static, str>, + /// The fully qualified path of the type that was received. received: Cow<'static, str>, }, } @@ -55,7 +66,7 @@ fn full_path( container_type_path: &str, ) -> alloc::string::String { match variant { - Some(variant) => format!("{}::{}::{}", container_type_path, variant, field), - None => format!("{}::{}", container_type_path, field), + Some(variant) => format!("{container_type_path}::{variant}::{field}"), + None => format!("{container_type_path}::{field}"), } } diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index 21d4ccd98a..53223835b3 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -82,6 +82,7 @@ pub struct UnnamedField { } impl UnnamedField { + /// Create a new [`UnnamedField`]. pub fn new(index: usize) -> Self { Self { index, @@ -135,7 +136,9 @@ impl UnnamedField { /// A representation of a field's accessor. #[derive(Clone, Debug, PartialEq, Eq)] pub enum FieldId { + /// Access a field by name. Named(Cow<'static, str>), + /// Access a field by index. Unnamed(usize), } diff --git a/crates/bevy_reflect/src/func/args/arg.rs b/crates/bevy_reflect/src/func/args/arg.rs index 8ca03aafd3..1c157a6b2f 100644 --- a/crates/bevy_reflect/src/func/args/arg.rs +++ b/crates/bevy_reflect/src/func/args/arg.rs @@ -196,8 +196,11 @@ impl<'a> Arg<'a> { /// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug)] pub enum ArgValue<'a> { + /// An owned argument. Owned(Box), + /// An immutable reference argument. Ref(&'a dyn PartialReflect), + /// A mutable reference argument. Mut(&'a mut dyn PartialReflect), } diff --git a/crates/bevy_reflect/src/func/args/error.rs b/crates/bevy_reflect/src/func/args/error.rs index bd32bd5e5a..20b6cd6220 100644 --- a/crates/bevy_reflect/src/func/args/error.rs +++ b/crates/bevy_reflect/src/func/args/error.rs @@ -12,15 +12,21 @@ pub enum ArgError { /// The argument is not the expected type. #[error("expected `{expected}` but received `{received}` (@ argument index {index})")] UnexpectedType { + /// Argument index. index: usize, + /// Expected argument type path. expected: Cow<'static, str>, + /// Received argument type path. received: Cow<'static, str>, }, /// The argument has the wrong ownership. #[error("expected {expected} value but received {received} value (@ argument index {index})")] InvalidOwnership { + /// Argument index. index: usize, + /// Expected ownership. expected: Ownership, + /// Received ownership. received: Ownership, }, /// Occurs when attempting to access an argument from an empty [`ArgList`]. diff --git a/crates/bevy_reflect/src/func/dynamic_function.rs b/crates/bevy_reflect/src/func/dynamic_function.rs index 7a5da57525..ab1d70e4ed 100644 --- a/crates/bevy_reflect/src/func/dynamic_function.rs +++ b/crates/bevy_reflect/src/func/dynamic_function.rs @@ -439,6 +439,7 @@ impl PartialReflect for DynamicFunction<'static> { } impl MaybeTyped for DynamicFunction<'static> {} + impl RegisterForReflection for DynamicFunction<'static> {} impl_type_path!((in bevy_reflect) DynamicFunction<'env>); @@ -550,7 +551,7 @@ mod tests { fn should_clone_dynamic_function() { let hello = String::from("Hello"); - let greet = |name: &String| -> String { format!("{}, {}!", hello, name) }; + let greet = |name: &String| -> String { format!("{hello}, {name}!") }; let greet = greet.into_function().with_name("greet"); let clone = greet.clone(); @@ -771,18 +772,18 @@ mod tests { #[test] fn should_debug_dynamic_function() { fn greet(name: &String) -> String { - format!("Hello, {}!", name) + format!("Hello, {name}!") } let function = greet.into_function(); - let debug = format!("{:?}", function); + let debug = format!("{function:?}"); assert_eq!(debug, "DynamicFunction(fn bevy_reflect::func::dynamic_function::tests::should_debug_dynamic_function::greet(_: &alloc::string::String) -> alloc::string::String)"); } #[test] fn should_debug_anonymous_dynamic_function() { let function = (|a: i32, b: i32| a + b).into_function(); - let debug = format!("{:?}", function); + let debug = format!("{function:?}"); assert_eq!(debug, "DynamicFunction(fn _(_: i32, _: i32) -> i32)"); } @@ -792,11 +793,11 @@ mod tests { a + b } - let func = add:: + let function = add:: .into_function() .with_overload(add::) .with_name("add"); - let debug = format!("{:?}", func); + let debug = format!("{function:?}"); assert_eq!( debug, "DynamicFunction(fn add{(_: i32, _: i32) -> i32, (_: f32, _: f32) -> f32})" diff --git a/crates/bevy_reflect/src/func/error.rs b/crates/bevy_reflect/src/func/error.rs index d9d105db1b..dc442e9da8 100644 --- a/crates/bevy_reflect/src/func/error.rs +++ b/crates/bevy_reflect/src/func/error.rs @@ -18,11 +18,18 @@ pub enum FunctionError { ArgError(#[from] ArgError), /// The number of arguments provided does not match the expected number. #[error("received {received} arguments but expected one of {expected:?}")] - ArgCountMismatch { expected: ArgCount, received: usize }, + ArgCountMismatch { + /// Expected argument count. [`ArgCount`] for overloaded functions will contain multiple possible counts. + expected: ArgCount, + /// Number of arguments received. + received: usize, + }, /// No overload was found for the given set of arguments. #[error("no overload found for arguments with signature `{received:?}`, expected one of `{expected:?}`")] NoOverload { + /// The set of available argument signatures. expected: HashSet, + /// The received argument signature. received: ArgumentSignature, }, } @@ -47,6 +54,9 @@ pub enum FunctionOverloadError { /// An error that occurs when attempting to add a function overload with a duplicate signature. #[error("could not add function overload: duplicate found for signature `{0:?}`")] DuplicateSignature(ArgumentSignature), + /// An attempt was made to add an overload with more than [`ArgCount::MAX_COUNT`] arguments. + /// + /// [`ArgCount::MAX_COUNT`]: crate::func::args::ArgCount #[error( "argument signature `{:?}` has too many arguments (max {})", 0, diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 53737fd891..2f5f82fbf5 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -235,6 +235,12 @@ impl TryFrom<[SignatureInfo; N]> for FunctionInfo { } } +/// Type information for the signature of a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// Every [`FunctionInfo`] contains one or more [`SignatureInfo`]s. +/// +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct SignatureInfo { name: Option>, @@ -434,7 +440,7 @@ impl<'a> Debug for PrettyPrintFunctionInfo<'a> { } match (self.include_name, self.info.name()) { - (true, Some(name)) => write!(f, "{}", name)?, + (true, Some(name)) => write!(f, "{name}")?, (true, None) => write!(f, "_")?, _ => {} } @@ -509,7 +515,7 @@ impl<'a> Debug for PrettyPrintSignatureInfo<'a> { } match (self.include_name, self.info.name()) { - (true, Some(name)) => write!(f, "{}", name)?, + (true, Some(name)) => write!(f, "{name}")?, (true, None) => write!(f, "_")?, _ => {} } diff --git a/crates/bevy_reflect/src/func/mod.rs b/crates/bevy_reflect/src/func/mod.rs index 74a89282c6..237bc9eafc 100644 --- a/crates/bevy_reflect/src/func/mod.rs +++ b/crates/bevy_reflect/src/func/mod.rs @@ -42,7 +42,7 @@ //! //! A "function" is a callable that does not capture its environment. //! These are typically defined with the `fn` keyword, which are referred to as _named_ functions. -//! But they are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. +//! But there are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. //! //! ```rust //! // This is a named function: diff --git a/crates/bevy_reflect/src/func/registry.rs b/crates/bevy_reflect/src/func/registry.rs index 58a8344ecf..08ed7bd7f1 100644 --- a/crates/bevy_reflect/src/func/registry.rs +++ b/crates/bevy_reflect/src/func/registry.rs @@ -336,6 +336,7 @@ impl Debug for FunctionRegistry { /// A synchronized wrapper around a [`FunctionRegistry`]. #[derive(Clone, Default, Debug)] pub struct FunctionRegistryArc { + /// The wrapped [`FunctionRegistry`]. pub internal: Arc>, } @@ -520,7 +521,7 @@ mod tests { let mut registry = FunctionRegistry::default(); registry.register_with_name("foo", foo).unwrap(); - let debug = format!("{:?}", registry); + let debug = format!("{registry:?}"); assert_eq!(debug, "{DynamicFunction(fn foo() -> i32)}"); } } diff --git a/crates/bevy_reflect/src/func/return_type.rs b/crates/bevy_reflect/src/func/return_type.rs index bab3c04b25..9abe0ef32c 100644 --- a/crates/bevy_reflect/src/func/return_type.rs +++ b/crates/bevy_reflect/src/func/return_type.rs @@ -129,7 +129,7 @@ macro_rules! impl_into_return { )? { fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { - $crate::func::Return::Owned(Box::new(self)) + $crate::func::Return::Owned(bevy_platform::prelude::Box::new(self)) } } diff --git a/crates/bevy_reflect/src/func/signature.rs b/crates/bevy_reflect/src/func/signature.rs index 7813d7d4f9..cedeaca952 100644 --- a/crates/bevy_reflect/src/func/signature.rs +++ b/crates/bevy_reflect/src/func/signature.rs @@ -90,6 +90,7 @@ impl<'a, 'b> ArgListSignature<'a, 'b> { } impl Eq for ArgListSignature<'_, '_> {} + impl PartialEq for ArgListSignature<'_, '_> { fn eq(&self, other: &Self) -> bool { self.len() == other.len() && self.iter().eq(other.iter()) @@ -229,7 +230,7 @@ mod tests { ); assert_eq!( - format!("{:?}", signature), + format!("{signature:?}"), "(&mut alloc::string::String, i32) -> ()" ); } diff --git a/crates/bevy_reflect/src/impls/alloc/borrow.rs b/crates/bevy_reflect/src/impls/alloc/borrow.rs new file mode 100644 index 0000000000..9021343c67 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/borrow.rs @@ -0,0 +1,308 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + list::{List, ListInfo, ListIter}, + prelude::*, + reflect::{impl_full_reflect, ApplyError}, + type_info::{MaybeTyped, OpaqueInfo, TypeInfo, Typed}, + type_registry::{ + FromType, GetTypeRegistration, ReflectDeserialize, ReflectFromPtr, ReflectSerialize, + TypeRegistration, TypeRegistry, + }, + utility::{reflect_hasher, GenericTypeInfoCell, NonGenericTypeInfoCell}, +}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use bevy_platform::prelude::*; +use bevy_reflect_derive::impl_type_path; +use core::any::Any; +use core::fmt; +use core::hash::{Hash, Hasher}; + +impl_type_path!(::alloc::borrow::Cow<'a: 'static, T: ToOwned + ?Sized>); + +impl PartialReflect for Cow<'static, str> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(self.clone())) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static + to_type: Self::type_path().into(), + }); + } + Ok(()) + } +} + +impl_full_reflect!(for Cow<'static, str>); + +impl Typed for Cow<'static, str> { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for Cow<'static, str> { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::>(); + registration.insert::(FromType::>::from_type()); + registration.insert::(FromType::>::from_type()); + registration.insert::(FromType::>::from_type()); + registration.insert::(FromType::>::from_type()); + registration + } +} + +impl FromReflect for Cow<'static, str> { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::>()?.clone()) + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, str>); + +impl List + for Cow<'static, [T]> +{ + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + self.as_ref().get(index).map(|x| x as &dyn PartialReflect) + } + + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + self.to_mut() + .get_mut(index) + .map(|x| x as &mut dyn PartialReflect) + } + + fn insert(&mut self, index: usize, element: Box) { + let value = T::take_from_reflect(element).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ); + }); + self.to_mut().insert(index, value); + } + + fn remove(&mut self, index: usize) -> Box { + Box::new(self.to_mut().remove(index)) + } + + fn push(&mut self, value: Box) { + let value = T::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to push invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.to_mut().push(value); + } + + fn pop(&mut self) -> Option> { + self.to_mut() + .pop() + .map(|value| Box::new(value) as Box) + } + + fn len(&self) -> usize { + self.as_ref().len() + } + + fn iter(&self) -> ListIter { + ListIter::new(self) + } + + fn drain(&mut self) -> Vec> { + self.to_mut() + .drain(..) + .map(|value| Box::new(value) as Box) + .collect() + } +} + +impl PartialReflect + for Cow<'static, [T]> +{ + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::List + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::List(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::List(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::List(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(self.clone())) + } + + fn reflect_hash(&self) -> Option { + crate::list_hash(self) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + crate::list_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::list_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } +} + +impl_full_reflect!( + for Cow<'static, [T]> + where + T: FromReflect + Clone + MaybeTyped + TypePath + GetTypeRegistration, +); + +impl Typed + for Cow<'static, [T]> +{ + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| TypeInfo::List(ListInfo::new::())) + } +} + +impl GetTypeRegistration + for Cow<'static, [T]> +{ + fn get_type_registration() -> TypeRegistration { + TypeRegistration::of::>() + } + + fn register_type_dependencies(registry: &mut TypeRegistry) { + registry.register::(); + } +} + +impl FromReflect + for Cow<'static, [T]> +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + let ref_list = reflect.reflect_ref().as_list().ok()?; + + let mut temp_vec = Vec::with_capacity(ref_list.len()); + + for field in ref_list.iter() { + temp_vec.push(T::from_reflect(field)?); + } + + Some(temp_vec.into()) + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, [T]>; ); diff --git a/crates/bevy_reflect/src/impls/alloc/collections/binary_heap.rs b/crates/bevy_reflect/src/impls/alloc/collections/binary_heap.rs new file mode 100644 index 0000000000..38f16f5851 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/binary_heap.rs @@ -0,0 +1,28 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::alloc::collections::BinaryHeap(Clone)); + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + use bevy_reflect::Reflect; + + #[test] + fn should_partial_eq_btree_map() { + let mut a = BTreeMap::new(); + a.insert(0usize, 1.23_f64); + let b = a.clone(); + let mut c = BTreeMap::new(); + c.insert(0usize, 3.21_f64); + + let a: &dyn Reflect = &a; + let b: &dyn Reflect = &b; + let c: &dyn Reflect = &c; + assert!(a + .reflect_partial_eq(b.as_partial_reflect()) + .unwrap_or_default()); + assert!(!a + .reflect_partial_eq(c.as_partial_reflect()) + .unwrap_or_default()); + } +} diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs new file mode 100644 index 0000000000..e579ace206 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/map.rs @@ -0,0 +1,254 @@ +use crate::{ + error::ReflectCloneError, + generics::{Generics, TypeParamInfo}, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + map::{map_apply, map_partial_eq, map_try_apply, Map, MapInfo, MapIter}, + prelude::*, + reflect::{impl_full_reflect, ApplyError}, + type_info::{MaybeTyped, TypeInfo, Typed}, + type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration}, + utility::GenericTypeInfoCell, +}; +use alloc::borrow::Cow; +use alloc::vec::Vec; +use bevy_platform::prelude::*; +use bevy_reflect_derive::impl_type_path; + +impl Map for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { + key.try_downcast_ref::() + .and_then(|key| Self::get(self, key)) + .map(|value| value as &dyn PartialReflect) + } + + fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { + key.try_downcast_ref::() + .and_then(move |key| Self::get_mut(self, key)) + .map(|value| value as &mut dyn PartialReflect) + } + + fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { + self.iter() + .nth(index) + .map(|(key, value)| (key as &dyn PartialReflect, value as &dyn PartialReflect)) + } + + fn get_at_mut( + &mut self, + index: usize, + ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { + self.iter_mut() + .nth(index) + .map(|(key, value)| (key as &dyn PartialReflect, value as &mut dyn PartialReflect)) + } + + fn len(&self) -> usize { + Self::len(self) + } + + fn iter(&self) -> MapIter { + MapIter::new(self) + } + + fn drain(&mut self) -> Vec<(Box, Box)> { + // BTreeMap doesn't have a `drain` function. See + // https://github.com/rust-lang/rust/issues/81074. So we have to fake one by popping + // elements off one at a time. + let mut result = Vec::with_capacity(self.len()); + while let Some((k, v)) = self.pop_first() { + result.push(( + Box::new(k) as Box, + Box::new(v) as Box, + )); + } + result + } + + fn insert_boxed( + &mut self, + key: Box, + value: Box, + ) -> Option> { + let key = K::take_from_reflect(key).unwrap_or_else(|key| { + panic!( + "Attempted to insert invalid key of type {}.", + key.reflect_type_path() + ) + }); + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(key, value) + .map(|old_value| Box::new(old_value) as Box) + } + + fn remove(&mut self, key: &dyn PartialReflect) -> Option> { + let mut from_reflect = None; + key.try_downcast_ref::() + .or_else(|| { + from_reflect = K::from_reflect(key); + from_reflect.as_ref() + }) + .and_then(|key| self.remove(key)) + .map(|value| Box::new(value) as Box) + } +} + +impl PartialReflect for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + #[inline] + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Map + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Map(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Map(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Map(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + let mut map = Self::new(); + for (key, value) in self.iter() { + let key = + key.reflect_clone()? + .take() + .map_err(|_| ReflectCloneError::FailedDowncast { + expected: Cow::Borrowed(::type_path()), + received: Cow::Owned(key.reflect_type_path().to_string()), + })?; + let value = + value + .reflect_clone()? + .take() + .map_err(|_| ReflectCloneError::FailedDowncast { + expected: Cow::Borrowed(::type_path()), + received: Cow::Owned(value.reflect_type_path().to_string()), + })?; + map.insert(key, value); + } + + Ok(Box::new(map)) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + map_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + map_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } +} + +impl_full_reflect!( + for ::alloc::collections::BTreeMap + where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +); + +impl Typed for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + TypeInfo::Map( + MapInfo::new::().with_generics(Generics::from_iter([ + TypeParamInfo::new::("K"), + TypeParamInfo::new::("V"), + ])), + ) + }) + } +} + +impl GetTypeRegistration for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for ::alloc::collections::BTreeMap +where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + let ref_map = reflect.reflect_ref().as_map().ok()?; + + let mut new_map = Self::new(); + + for (key, value) in ref_map.iter() { + let new_key = K::from_reflect(key)?; + let new_value = V::from_reflect(value)?; + new_map.insert(new_key, new_value); + } + + Some(new_map) + } +} + +impl_type_path!(::alloc::collections::BTreeMap); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::alloc::collections::BTreeMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + > +); diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/mod.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/mod.rs new file mode 100644 index 0000000000..095ca5dd2e --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/mod.rs @@ -0,0 +1,2 @@ +mod map; +mod set; diff --git a/crates/bevy_reflect/src/impls/alloc/collections/btree/set.rs b/crates/bevy_reflect/src/impls/alloc/collections/btree/set.rs new file mode 100644 index 0000000000..41d711f205 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/btree/set.rs @@ -0,0 +1,3 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::alloc::collections::BTreeSet(Clone)); diff --git a/crates/bevy_reflect/src/impls/alloc/collections/mod.rs b/crates/bevy_reflect/src/impls/alloc/collections/mod.rs new file mode 100644 index 0000000000..dcf75ce03c --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/mod.rs @@ -0,0 +1,3 @@ +mod binary_heap; +mod btree; +mod vec_deque; diff --git a/crates/bevy_reflect/src/impls/alloc/collections/vec_deque.rs b/crates/bevy_reflect/src/impls/alloc/collections/vec_deque.rs new file mode 100644 index 0000000000..5144bc3c2b --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/collections/vec_deque.rs @@ -0,0 +1,20 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_veclike; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; + +impl_reflect_for_veclike!( + ::alloc::collections::VecDeque, + ::alloc::collections::VecDeque::insert, + ::alloc::collections::VecDeque::remove, + ::alloc::collections::VecDeque::push_back, + ::alloc::collections::VecDeque::pop_back, + ::alloc::collections::VecDeque:: +); +impl_type_path!(::alloc::collections::VecDeque); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::alloc::collections::VecDeque; ); diff --git a/crates/bevy_reflect/src/impls/alloc/mod.rs b/crates/bevy_reflect/src/impls/alloc/mod.rs new file mode 100644 index 0000000000..a49fa5599f --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/mod.rs @@ -0,0 +1,4 @@ +mod borrow; +mod collections; +mod string; +mod vec; diff --git a/crates/bevy_reflect/src/impls/alloc/string.rs b/crates/bevy_reflect/src/impls/alloc/string.rs new file mode 100644 index 0000000000..627082ee75 --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/string.rs @@ -0,0 +1,30 @@ +use crate::{ + std_traits::ReflectDefault, + type_registry::{ReflectDeserialize, ReflectSerialize}, +}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::alloc::string::String( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); + +#[cfg(test)] +mod tests { + use alloc::string::String; + use bevy_reflect::PartialReflect; + + #[test] + fn should_partial_eq_string() { + let a: &dyn PartialReflect = &String::from("Hello"); + let b: &dyn PartialReflect = &String::from("Hello"); + let c: &dyn PartialReflect = &String::from("World"); + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } +} diff --git a/crates/bevy_reflect/src/impls/alloc/vec.rs b/crates/bevy_reflect/src/impls/alloc/vec.rs new file mode 100644 index 0000000000..4e4c819d6a --- /dev/null +++ b/crates/bevy_reflect/src/impls/alloc/vec.rs @@ -0,0 +1,35 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_veclike; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; + +impl_reflect_for_veclike!( + ::alloc::vec::Vec, + ::alloc::vec::Vec::insert, + ::alloc::vec::Vec::remove, + ::alloc::vec::Vec::push, + ::alloc::vec::Vec::pop, + [T] +); +impl_type_path!(::alloc::vec::Vec); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::alloc::vec::Vec; ); + +#[cfg(test)] +mod tests { + use alloc::vec; + use bevy_reflect::PartialReflect; + + #[test] + fn should_partial_eq_vec() { + let a: &dyn PartialReflect = &vec![1, 2, 3]; + let b: &dyn PartialReflect = &vec![1, 2, 3]; + let c: &dyn PartialReflect = &vec![3, 2, 1]; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } +} diff --git a/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_map.rs b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_map.rs new file mode 100644 index 0000000000..48f0ddcd89 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_map.rs @@ -0,0 +1,47 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashmap; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashmap!(bevy_platform::collections::HashMap); +impl_type_path!(::bevy_platform::collections::HashMap); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +#[cfg(test)] +mod tests { + use crate::{PartialReflect, Reflect}; + use static_assertions::assert_impl_all; + + #[test] + fn should_partial_eq_hash_map() { + let mut a = >::default(); + a.insert(0usize, 1.23_f64); + let b = a.clone(); + let mut c = >::default(); + c.insert(0usize, 3.21_f64); + + let a: &dyn PartialReflect = &a; + let b: &dyn PartialReflect = &b; + let c: &dyn PartialReflect = &c; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn should_reflect_hashmaps() { + assert_impl_all!(bevy_platform::collections::HashMap: Reflect); + } +} diff --git a/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_set.rs b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_set.rs new file mode 100644 index 0000000000..a9c3f7a959 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/collections/hash_set.rs @@ -0,0 +1,17 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashset; +#[cfg(feature = "functions")] +use crate::{from_reflect::FromReflect, type_path::TypePath, type_registry::GetTypeRegistration}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashset!(::bevy_platform::collections::HashSet); +impl_type_path!(::bevy_platform::collections::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); diff --git a/crates/bevy_reflect/src/impls/bevy_platform/collections/mod.rs b/crates/bevy_reflect/src/impls/bevy_platform/collections/mod.rs new file mode 100644 index 0000000000..2bde6a0653 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/collections/mod.rs @@ -0,0 +1,2 @@ +mod hash_map; +mod hash_set; diff --git a/crates/bevy_reflect/src/impls/bevy_platform/hash.rs b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs new file mode 100644 index 0000000000..c2a38dd5f3 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/hash.rs @@ -0,0 +1,5 @@ +use bevy_reflect_derive::impl_type_path; + +impl_type_path!(::bevy_platform::hash::NoOpHash); +impl_type_path!(::bevy_platform::hash::FixedHasher); +impl_type_path!(::bevy_platform::hash::PassHash); diff --git a/crates/bevy_reflect/src/impls/bevy_platform/mod.rs b/crates/bevy_reflect/src/impls/bevy_platform/mod.rs new file mode 100644 index 0000000000..aa7f15eb5e --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/mod.rs @@ -0,0 +1,4 @@ +mod collections; +mod hash; +mod sync; +mod time; diff --git a/crates/bevy_reflect/src/impls/bevy_platform/sync.rs b/crates/bevy_reflect/src/impls/bevy_platform/sync.rs new file mode 100644 index 0000000000..ba3234177f --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/sync.rs @@ -0,0 +1,3 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::bevy_platform::sync::Arc(Clone)); diff --git a/crates/bevy_reflect/src/impls/bevy_platform/time.rs b/crates/bevy_reflect/src/impls/bevy_platform/time.rs new file mode 100644 index 0000000000..7120359dc4 --- /dev/null +++ b/crates/bevy_reflect/src/impls/bevy_platform/time.rs @@ -0,0 +1,18 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::bevy_platform::time::Instant( + Clone, Debug, Hash, PartialEq +)); + +#[cfg(test)] +mod tests { + use crate::FromReflect; + use bevy_platform::time::Instant; + + #[test] + fn instant_should_from_reflect() { + let expected = Instant::now(); + let output = Instant::from_reflect(&expected).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/bevy_reflect/src/impls/core/any.rs b/crates/bevy_reflect/src/impls/core/any.rs new file mode 100644 index 0000000000..062768c341 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/any.rs @@ -0,0 +1,15 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::any::TypeId(Clone, Debug, Hash, PartialEq,)); + +#[cfg(test)] +mod tests { + use bevy_reflect::FromReflect; + + #[test] + fn type_id_should_from_reflect() { + let type_id = core::any::TypeId::of::(); + let output = ::from_reflect(&type_id).unwrap(); + assert_eq!(type_id, output); + } +} diff --git a/crates/bevy_reflect/src/impls/core/hash.rs b/crates/bevy_reflect/src/impls/core/hash.rs new file mode 100644 index 0000000000..6301400091 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/hash.rs @@ -0,0 +1,3 @@ +use bevy_reflect_derive::impl_type_path; + +impl_type_path!(::core::hash::BuildHasherDefault); diff --git a/crates/bevy_reflect/src/impls/core/mod.rs b/crates/bevy_reflect/src/impls/core/mod.rs new file mode 100644 index 0000000000..cf20471410 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/mod.rs @@ -0,0 +1,11 @@ +mod any; +mod hash; +mod net; +mod num; +mod ops; +mod option; +mod panic; +mod primitives; +mod result; +mod sync; +mod time; diff --git a/crates/bevy_reflect/src/impls/core/net.rs b/crates/bevy_reflect/src/impls/core/net.rs new file mode 100644 index 0000000000..965308680b --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/net.rs @@ -0,0 +1,11 @@ +use crate::type_registry::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::net::SocketAddr( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); diff --git a/crates/bevy_reflect/src/impls/core/num.rs b/crates/bevy_reflect/src/impls/core/num.rs new file mode 100644 index 0000000000..e64744f69e --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/num.rs @@ -0,0 +1,115 @@ +use crate::type_registry::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::num::NonZeroI128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroIsize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroUsize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroU8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::NonZeroI8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); +impl_reflect_opaque!(::core::num::Wrapping(Clone)); +impl_reflect_opaque!(::core::num::Saturating(Clone)); + +#[cfg(test)] +mod tests { + use bevy_reflect::{FromReflect, PartialReflect}; + + #[test] + fn nonzero_usize_impl_reflect_from_reflect() { + let a: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); + let b: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + let forty_two: core::num::NonZero = FromReflect::from_reflect(a).unwrap(); + assert_eq!(forty_two, core::num::NonZero::::new(42).unwrap()); + } +} diff --git a/crates/bevy_reflect/src/impls/core/ops.rs b/crates/bevy_reflect/src/impls/core/ops.rs new file mode 100644 index 0000000000..fdbe87a802 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/ops.rs @@ -0,0 +1,9 @@ +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::ops::Range(Clone)); +impl_reflect_opaque!(::core::ops::RangeInclusive(Clone)); +impl_reflect_opaque!(::core::ops::RangeFrom(Clone)); +impl_reflect_opaque!(::core::ops::RangeTo(Clone)); +impl_reflect_opaque!(::core::ops::RangeToInclusive(Clone)); +impl_reflect_opaque!(::core::ops::RangeFull(Clone)); +impl_reflect_opaque!(::core::ops::Bound(Clone)); diff --git a/crates/bevy_reflect/src/impls/core/option.rs b/crates/bevy_reflect/src/impls/core/option.rs new file mode 100644 index 0000000000..fe8318806d --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/option.rs @@ -0,0 +1,138 @@ +#![expect( + unused_qualifications, + reason = "the macro uses `MyEnum::Variant` which is generally unnecessary for `Option`" +)] + +use bevy_reflect_derive::impl_reflect; + +impl_reflect! { + #[type_path = "core::option"] + enum Option { + None, + Some(T), + } +} + +#[cfg(test)] +mod tests { + use crate::{Enum, FromReflect, PartialReflect, TypeInfo, Typed, VariantInfo, VariantType}; + use bevy_reflect_derive::Reflect; + use static_assertions::assert_impl_all; + + #[test] + fn should_partial_eq_option() { + let a: &dyn PartialReflect = &Some(123); + let b: &dyn PartialReflect = &Some(123); + assert_eq!(Some(true), a.reflect_partial_eq(b)); + } + + #[test] + fn option_should_impl_enum() { + assert_impl_all!(Option<()>: Enum); + + let mut value = Some(123usize); + + assert!(value + .reflect_partial_eq(&Some(123usize)) + .unwrap_or_default()); + assert!(!value + .reflect_partial_eq(&Some(321usize)) + .unwrap_or_default()); + + assert_eq!("Some", value.variant_name()); + assert_eq!("core::option::Option::Some", value.variant_path()); + + if value.is_variant(VariantType::Tuple) { + if let Some(field) = value + .field_at_mut(0) + .and_then(|field| field.try_downcast_mut::()) + { + *field = 321; + } + } else { + panic!("expected `VariantType::Tuple`"); + } + + assert_eq!(Some(321), value); + } + + #[test] + fn option_should_from_reflect() { + #[derive(Reflect, PartialEq, Debug)] + struct Foo(usize); + + let expected = Some(Foo(123)); + let output = as FromReflect>::from_reflect(&expected).unwrap(); + + assert_eq!(expected, output); + } + + #[test] + fn option_should_apply() { + #[derive(Reflect, PartialEq, Debug)] + struct Foo(usize); + + // === None on None === // + let patch = None::; + let mut value = None::; + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "None apply onto None"); + + // === Some on None === // + let patch = Some(Foo(123)); + let mut value = None::; + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "Some apply onto None"); + + // === None on Some === // + let patch = None::; + let mut value = Some(Foo(321)); + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "None apply onto Some"); + + // === Some on Some === // + let patch = Some(Foo(123)); + let mut value = Some(Foo(321)); + PartialReflect::apply(&mut value, &patch); + + assert_eq!(patch, value, "Some apply onto Some"); + } + + #[test] + fn option_should_impl_typed() { + assert_impl_all!(Option<()>: Typed); + + type MyOption = Option; + let info = MyOption::type_info(); + if let TypeInfo::Enum(info) = info { + assert_eq!( + "None", + info.variant_at(0).unwrap().name(), + "Expected `None` to be variant at index `0`" + ); + assert_eq!( + "Some", + info.variant_at(1).unwrap().name(), + "Expected `Some` to be variant at index `1`" + ); + assert_eq!("Some", info.variant("Some").unwrap().name()); + if let VariantInfo::Tuple(variant) = info.variant("Some").unwrap() { + assert!( + variant.field_at(0).unwrap().is::(), + "Expected `Some` variant to contain `i32`" + ); + assert!( + variant.field_at(1).is_none(), + "Expected `Some` variant to only contain 1 field" + ); + } else { + panic!("Expected `VariantInfo::Tuple`"); + } + } else { + panic!("Expected `TypeInfo::Enum`"); + } + } +} diff --git a/crates/bevy_reflect/src/impls/core/panic.rs b/crates/bevy_reflect/src/impls/core/panic.rs new file mode 100644 index 0000000000..75bf365422 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/panic.rs @@ -0,0 +1,158 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::ApplyError, + type_info::{OpaqueInfo, TypeInfo, Typed}, + type_path::DynamicTypePath, + type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration}, + utility::{reflect_hasher, NonGenericTypeInfoCell}, +}; +use bevy_platform::prelude::*; +use core::any::Any; +use core::hash::{Hash, Hasher}; +use core::panic::Location; + +impl TypePath for &'static Location<'static> { + fn type_path() -> &'static str { + "core::panic::Location" + } + + fn short_type_path() -> &'static str { + "Location" + } +} + +impl PartialReflect for &'static Location<'static> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(*self)) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for &'static Location<'static> { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for &'static Location<'static> { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for &'static Location<'static> { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for &'static Location<'static> { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static Location<'static>); diff --git a/crates/bevy_reflect/src/impls/core/primitives.rs b/crates/bevy_reflect/src/impls/core/primitives.rs new file mode 100644 index 0000000000..3600f2ece5 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/primitives.rs @@ -0,0 +1,565 @@ +use crate::{ + array::{Array, ArrayInfo, ArrayIter}, + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::ApplyError, + type_info::{MaybeTyped, OpaqueInfo, TypeInfo, Typed}, + type_registry::{ + FromType, GetTypeRegistration, ReflectDeserialize, ReflectFromPtr, ReflectSerialize, + TypeRegistration, TypeRegistry, + }, + utility::{reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell}, +}; +use bevy_platform::prelude::*; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; +use core::any::Any; +use core::fmt; +use core::hash::{Hash, Hasher}; + +impl_reflect_opaque!(bool( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(char( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(u128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(usize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i8( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i16( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i32( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i64( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(i128( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(isize( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(f32( + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_reflect_opaque!(f64( + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + Default +)); +impl_type_path!(str); + +impl PartialReflect for &'static str { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(*self)) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self, f) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: Self::type_path().into(), + }); + } + Ok(()) + } +} + +impl Reflect for &'static str { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for &'static str { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for &'static str { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for &'static str { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static str); + +impl Array for [T; N] { + #[inline] + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + <[T]>::get(self, index).map(|value| value as &dyn PartialReflect) + } + + #[inline] + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + <[T]>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) + } + + #[inline] + fn len(&self) -> usize { + N + } + + #[inline] + fn iter(&self) -> ArrayIter { + ArrayIter::new(self) + } + + #[inline] + fn drain(self: Box) -> Vec> { + self.into_iter() + .map(|value| Box::new(value) as Box) + .collect() + } +} + +impl PartialReflect + for [T; N] +{ + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + #[inline] + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Array + } + + #[inline] + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Array(self) + } + + #[inline] + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Array(self) + } + + #[inline] + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Array(self) + } + + #[inline] + fn reflect_hash(&self) -> Option { + crate::array_hash(self) + } + + #[inline] + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + crate::array_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::array_apply(self, value); + } + + #[inline] + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::array_try_apply(self, value) + } +} + +impl Reflect for [T; N] { + #[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(()) + } +} + +impl FromReflect + for [T; N] +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + let ref_array = reflect.reflect_ref().as_array().ok()?; + + let mut temp_vec = Vec::with_capacity(ref_array.len()); + + for field in ref_array.iter() { + temp_vec.push(T::from_reflect(field)?); + } + + temp_vec.try_into().ok() + } +} + +impl Typed for [T; N] { + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| TypeInfo::Array(ArrayInfo::new::(N))) + } +} + +impl TypePath for [T; N] { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::short_type_path())) + } +} + +impl GetTypeRegistration + for [T; N] +{ + fn get_type_registration() -> TypeRegistration { + TypeRegistration::of::<[T; N]>() + } + + fn register_type_dependencies(registry: &mut TypeRegistry) { + registry.register::(); + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!([T; N]; [const N: usize]); + +impl TypePath for [T] +where + [T]: ToOwned, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{}]", ::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("[{}]", ::short_type_path())) + } +} + +impl TypePath for &'static T { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&{}", T::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&{}", T::short_type_path())) + } +} + +impl TypePath for &'static mut T { + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&mut {}", T::type_path())) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| format!("&mut {}", T::short_type_path())) + } +} + +#[cfg(test)] +mod tests { + use bevy_reflect::{FromReflect, PartialReflect}; + use core::f32::consts::{PI, TAU}; + + #[test] + fn should_partial_eq_char() { + let a: &dyn PartialReflect = &'x'; + let b: &dyn PartialReflect = &'x'; + let c: &dyn PartialReflect = &'o'; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn should_partial_eq_i32() { + let a: &dyn PartialReflect = &123_i32; + let b: &dyn PartialReflect = &123_i32; + let c: &dyn PartialReflect = &321_i32; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn should_partial_eq_f32() { + let a: &dyn PartialReflect = &PI; + let b: &dyn PartialReflect = &PI; + let c: &dyn PartialReflect = &TAU; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn static_str_should_from_reflect() { + let expected = "Hello, World!"; + let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/bevy_reflect/src/impls/core/result.rs b/crates/bevy_reflect/src/impls/core/result.rs new file mode 100644 index 0000000000..d601e0bd6f --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/result.rs @@ -0,0 +1,14 @@ +#![expect( + unused_qualifications, + reason = "the macro uses `MyEnum::Variant` which is generally unnecessary for `Result`" +)] + +use bevy_reflect_derive::impl_reflect; + +impl_reflect! { + #[type_path = "core::result"] + enum Result { + Ok(T), + Err(E), + } +} diff --git a/crates/bevy_reflect/src/impls/core/sync.rs b/crates/bevy_reflect/src/impls/core/sync.rs new file mode 100644 index 0000000000..b4fe8977d5 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/sync.rs @@ -0,0 +1,192 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::{impl_full_reflect, ApplyError}, + type_info::{OpaqueInfo, TypeInfo, Typed}, + type_path::DynamicTypePath, + type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration}, + utility::NonGenericTypeInfoCell, +}; +use bevy_platform::prelude::*; +use bevy_reflect_derive::impl_type_path; +use core::any::Any; +use core::fmt; + +macro_rules! impl_reflect_for_atomic { + ($ty:ty, $ordering:expr) => { + impl_type_path!($ty); + + const _: () = { + #[cfg(feature = "functions")] + crate::func::macros::impl_function_traits!($ty); + + impl GetTypeRegistration for $ty + where + $ty: Any + Send + Sync, + { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + + // Serde only supports atomic types when the "std" feature is enabled + #[cfg(feature = "std")] + { + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + } + + registration + } + } + + impl Typed for $ty + where + $ty: Any + Send + Sync, + { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| { + let info = OpaqueInfo::new::(); + TypeInfo::Opaque(info) + }) + } + } + + impl PartialReflect for $ty + where + $ty: Any + Send + Sync, + { + #[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 + } + #[inline] + 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) + } + + #[inline] + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(<$ty>::new(self.load($ordering)))) + } + + #[inline] + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + *self = <$ty>::new(value.load($ordering)); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: Into::into(DynamicTypePath::reflect_type_path(value)), + to_type: Into::into(::type_path()), + }); + } + Ok(()) + } + #[inline] + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + #[inline] + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + #[inline] + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + #[inline] + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } + } + + impl FromReflect for $ty + where + $ty: Any + Send + Sync, + { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(<$ty>::new( + reflect.try_downcast_ref::<$ty>()?.load($ordering), + )) + } + } + }; + + impl_full_reflect!(for $ty where $ty: Any + Send + Sync); + }; +} + +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicIsize, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicUsize, + ::core::sync::atomic::Ordering::SeqCst +); +#[cfg(target_has_atomic = "64")] +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI64, + ::core::sync::atomic::Ordering::SeqCst +); +#[cfg(target_has_atomic = "64")] +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU64, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI32, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU32, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI16, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU16, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicI8, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicU8, + ::core::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::core::sync::atomic::AtomicBool, + ::core::sync::atomic::Ordering::SeqCst +); diff --git a/crates/bevy_reflect/src/impls/core/time.rs b/crates/bevy_reflect/src/impls/core/time.rs new file mode 100644 index 0000000000..42e581b864 --- /dev/null +++ b/crates/bevy_reflect/src/impls/core/time.rs @@ -0,0 +1,32 @@ +use crate::{ + std_traits::ReflectDefault, + type_registry::{ReflectDeserialize, ReflectSerialize}, +}; +use bevy_reflect_derive::impl_reflect_opaque; + +impl_reflect_opaque!(::core::time::Duration( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); + +#[cfg(test)] +mod tests { + use bevy_reflect::{ReflectSerialize, TypeRegistry}; + use core::time::Duration; + + #[test] + fn can_serialize_duration() { + let mut type_registry = TypeRegistry::default(); + type_registry.register::(); + + let reflect_serialize = type_registry + .get_type_data::(core::any::TypeId::of::()) + .unwrap(); + let _serializable = reflect_serialize.get_serializable(&Duration::ZERO); + } +} diff --git a/crates/bevy_reflect/src/impls/hashbrown.rs b/crates/bevy_reflect/src/impls/hashbrown.rs new file mode 100644 index 0000000000..4ea914dc3f --- /dev/null +++ b/crates/bevy_reflect/src/impls/hashbrown.rs @@ -0,0 +1,43 @@ +use crate::impls::macros::{impl_reflect_for_hashmap, impl_reflect_for_hashset}; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; +use bevy_reflect_derive::impl_type_path; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashmap!(hashbrown::hash_map::HashMap); +impl_type_path!(::hashbrown::hash_map::HashMap); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::hashbrown::hash_map::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +impl_reflect_for_hashset!(::hashbrown::hash_set::HashSet); +impl_type_path!(::hashbrown::hash_set::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::hashbrown::hash_set::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +#[cfg(test)] +mod tests { + use crate::Reflect; + use static_assertions::assert_impl_all; + + #[test] + fn should_reflect_hashmaps() { + // We specify `foldhash::fast::RandomState` directly here since without the `default-hasher` + // feature, hashbrown uses an empty enum to force users to specify their own + assert_impl_all!(hashbrown::HashMap: Reflect); + } +} diff --git a/crates/bevy_reflect/src/impls/macros/list.rs b/crates/bevy_reflect/src/impls/macros/list.rs new file mode 100644 index 0000000000..72e05ba10f --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/list.rs @@ -0,0 +1,192 @@ +macro_rules! impl_reflect_for_veclike { + ($ty:ty, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { + const _: () = { + impl $crate::list::List for $ty { + #[inline] + fn get(&self, index: usize) -> Option<&dyn $crate::reflect::PartialReflect> { + <$sub>::get(self, index).map(|value| value as &dyn $crate::reflect::PartialReflect) + } + + #[inline] + fn get_mut(&mut self, index: usize) -> Option<&mut dyn $crate::reflect::PartialReflect> { + <$sub>::get_mut(self, index).map(|value| value as &mut dyn $crate::reflect::PartialReflect) + } + + fn insert(&mut self, index: usize, value: bevy_platform::prelude::Box) { + let value = value.try_take::().unwrap_or_else(|value| { + T::from_reflect(&*value).unwrap_or_else(|| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }) + }); + $insert(self, index, value); + } + + fn remove(&mut self, index: usize) -> bevy_platform::prelude::Box { + bevy_platform::prelude::Box::new($remove(self, index)) + } + + fn push(&mut self, value: bevy_platform::prelude::Box) { + let value = T::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to push invalid value of type {}.", + value.reflect_type_path() + ) + }); + $push(self, value); + } + + fn pop(&mut self) -> Option> { + $pop(self).map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + } + + #[inline] + fn len(&self) -> usize { + <$sub>::len(self) + } + + #[inline] + fn iter(&self) -> $crate::list::ListIter { + $crate::list::ListIter::new(self) + } + + #[inline] + fn drain(&mut self) -> alloc::vec::Vec> { + self.drain(..) + .map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + .collect() + } + } + + impl $crate::reflect::PartialReflect for $ty { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static $crate::type_info::TypeInfo> { + Some(::type_info()) + } + + fn into_partial_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { + self + } + + #[inline] + fn as_partial_reflect(&self) -> &dyn $crate::reflect::PartialReflect { + self + } + + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn $crate::reflect::PartialReflect { + self + } + + fn try_into_reflect( + self: bevy_platform::prelude::Box, + ) -> Result, bevy_platform::prelude::Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn $crate::reflect::Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn $crate::reflect::Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> $crate::kind::ReflectKind { + $crate::kind::ReflectKind::List + } + + fn reflect_ref(&self) -> $crate::kind::ReflectRef { + $crate::kind::ReflectRef::List(self) + } + + fn reflect_mut(&mut self) -> $crate::kind::ReflectMut { + $crate::kind::ReflectMut::List(self) + } + + fn reflect_owned(self: bevy_platform::prelude::Box) -> $crate::kind::ReflectOwned { + $crate::kind::ReflectOwned::List(self) + } + + fn reflect_clone(&self) -> Result, $crate::error::ReflectCloneError> { + Ok(bevy_platform::prelude::Box::new( + self.iter() + .map(|value| { + value.reflect_clone()?.take().map_err(|_| { + $crate::error::ReflectCloneError::FailedDowncast { + expected: alloc::borrow::Cow::Borrowed(::type_path()), + received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(value.reflect_type_path())), + } + }) + }) + .collect::>()?, + )) + } + + fn reflect_hash(&self) -> Option { + $crate::list::list_hash(self) + } + + fn reflect_partial_eq(&self, value: &dyn $crate::reflect::PartialReflect) -> Option { + $crate::list::list_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn $crate::reflect::PartialReflect) { + $crate::list::list_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn $crate::reflect::PartialReflect) -> Result<(), $crate::reflect::ApplyError> { + $crate::list::list_try_apply(self, value) + } + } + + $crate::impl_full_reflect!( for $ty where T: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration); + + impl $crate::type_info::Typed for $ty { + fn type_info() -> &'static $crate::type_info::TypeInfo { + static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + $crate::type_info::TypeInfo::List( + $crate::list::ListInfo::new::().with_generics($crate::generics::Generics::from_iter([ + $crate::generics::TypeParamInfo::new::("T") + ])) + ) + }) + } + } + + impl $crate::type_registry::GetTypeRegistration + for $ty + { + fn get_type_registration() -> $crate::type_registry::TypeRegistration { + let mut registration = $crate::type_registry::TypeRegistration::of::<$ty>(); + registration.insert::<$crate::type_registry::ReflectFromPtr>($crate::type_registry::FromType::<$ty>::from_type()); + registration.insert::<$crate::from_reflect::ReflectFromReflect>($crate::type_registry::FromType::<$ty>::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut $crate::type_registry::TypeRegistry) { + registry.register::(); + } + } + + impl $crate::from_reflect::FromReflect for $ty { + fn from_reflect(reflect: &dyn $crate::reflect::PartialReflect) -> Option { + let ref_list = reflect.reflect_ref().as_list().ok()?; + + let mut new_list = Self::with_capacity(ref_list.len()); + + for field in ref_list.iter() { + $push(&mut new_list, T::from_reflect(field)?); + } + + Some(new_list) + } + } + }; + }; +} + +pub(crate) use impl_reflect_for_veclike; diff --git a/crates/bevy_reflect/src/impls/macros/map.rs b/crates/bevy_reflect/src/impls/macros/map.rs new file mode 100644 index 0000000000..ce621b7f78 --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/map.rs @@ -0,0 +1,261 @@ +macro_rules! impl_reflect_for_hashmap { + ($ty:path) => { + const _: () = { + impl $crate::map::Map for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get(&self, key: &dyn $crate::reflect::PartialReflect) -> Option<&dyn $crate::reflect::PartialReflect> { + key.try_downcast_ref::() + .and_then(|key| Self::get(self, key)) + .map(|value| value as &dyn $crate::reflect::PartialReflect) + } + + fn get_mut(&mut self, key: &dyn $crate::reflect::PartialReflect) -> Option<&mut dyn $crate::reflect::PartialReflect> { + key.try_downcast_ref::() + .and_then(move |key| Self::get_mut(self, key)) + .map(|value| value as &mut dyn $crate::reflect::PartialReflect) + } + + fn get_at(&self, index: usize) -> Option<(&dyn $crate::reflect::PartialReflect, &dyn $crate::reflect::PartialReflect)> { + self.iter() + .nth(index) + .map(|(key, value)| (key as &dyn $crate::reflect::PartialReflect, value as &dyn $crate::reflect::PartialReflect)) + } + + fn get_at_mut( + &mut self, + index: usize, + ) -> Option<(&dyn $crate::reflect::PartialReflect, &mut dyn $crate::reflect::PartialReflect)> { + self.iter_mut().nth(index).map(|(key, value)| { + (key as &dyn $crate::reflect::PartialReflect, value as &mut dyn $crate::reflect::PartialReflect) + }) + } + + fn len(&self) -> usize { + Self::len(self) + } + + fn iter(&self) -> $crate::map::MapIter { + $crate::map::MapIter::new(self) + } + + fn drain(&mut self) -> bevy_platform::prelude::Vec<(bevy_platform::prelude::Box, bevy_platform::prelude::Box)> { + self.drain() + .map(|(key, value)| { + ( + bevy_platform::prelude::Box::new(key) as bevy_platform::prelude::Box, + bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box, + ) + }) + .collect() + } + + fn to_dynamic_map(&self) -> $crate::map::DynamicMap { + let mut dynamic_map = $crate::map::DynamicMap::default(); + dynamic_map.set_represented_type($crate::reflect::PartialReflect::get_represented_type_info(self)); + for (k, v) in self { + let key = K::from_reflect(k).unwrap_or_else(|| { + panic!( + "Attempted to clone invalid key of type {}.", + k.reflect_type_path() + ) + }); + dynamic_map.insert_boxed(bevy_platform::prelude::Box::new(key), v.to_dynamic()); + } + dynamic_map + } + + fn insert_boxed( + &mut self, + key: bevy_platform::prelude::Box, + value: bevy_platform::prelude::Box, + ) -> Option> { + let key = K::take_from_reflect(key).unwrap_or_else(|key| { + panic!( + "Attempted to insert invalid key of type {}.", + key.reflect_type_path() + ) + }); + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(key, value) + .map(|old_value| bevy_platform::prelude::Box::new(old_value) as bevy_platform::prelude::Box) + } + + fn remove(&mut self, key: &dyn $crate::reflect::PartialReflect) -> Option> { + let mut from_reflect = None; + key.try_downcast_ref::() + .or_else(|| { + from_reflect = K::from_reflect(key); + from_reflect.as_ref() + }) + .and_then(|key| self.remove(key)) + .map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + } + } + + impl $crate::reflect::PartialReflect for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get_represented_type_info(&self) -> Option<&'static $crate::type_info::TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { + self + } + + fn as_partial_reflect(&self) -> &dyn $crate::reflect::PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn $crate::reflect::PartialReflect { + self + } + + fn try_into_reflect( + self: bevy_platform::prelude::Box, + ) -> Result, bevy_platform::prelude::Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn $crate::reflect::Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn $crate::reflect::Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> $crate::kind::ReflectKind { + $crate::kind::ReflectKind::Map + } + + fn reflect_ref(&self) -> $crate::kind::ReflectRef { + $crate::kind::ReflectRef::Map(self) + } + + fn reflect_mut(&mut self) -> $crate::kind::ReflectMut { + $crate::kind::ReflectMut::Map(self) + } + + fn reflect_owned(self: bevy_platform::prelude::Box) -> $crate::kind::ReflectOwned { + $crate::kind::ReflectOwned::Map(self) + } + + fn reflect_clone(&self) -> Result, $crate::error::ReflectCloneError> { + let mut map = Self::with_capacity_and_hasher(self.len(), S::default()); + for (key, value) in self.iter() { + let key = key.reflect_clone()?.take().map_err(|_| { + $crate::error::ReflectCloneError::FailedDowncast { + expected: alloc::borrow::Cow::Borrowed(::type_path()), + received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(key.reflect_type_path())), + } + })?; + let value = value.reflect_clone()?.take().map_err(|_| { + $crate::error::ReflectCloneError::FailedDowncast { + expected: alloc::borrow::Cow::Borrowed(::type_path()), + received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(value.reflect_type_path())), + } + })?; + map.insert(key, value); + } + + Ok(bevy_platform::prelude::Box::new(map)) + } + + fn reflect_partial_eq(&self, value: &dyn $crate::reflect::PartialReflect) -> Option { + $crate::map::map_partial_eq(self, value) + } + + fn apply(&mut self, value: &dyn $crate::reflect::PartialReflect) { + $crate::map::map_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn $crate::reflect::PartialReflect) -> Result<(), $crate::reflect::ApplyError> { + $crate::map::map_try_apply(self, value) + } + } + + $crate::impl_full_reflect!( + for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + ); + + impl $crate::type_info::Typed for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn type_info() -> &'static $crate::type_info::TypeInfo { + static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + $crate::type_info::TypeInfo::Map( + $crate::map::MapInfo::new::().with_generics($crate::generics::Generics::from_iter([ + $crate::generics::TypeParamInfo::new::("K"), + $crate::generics::TypeParamInfo::new::("V"), + ])), + ) + }) + } + } + + impl $crate::type_registry::GetTypeRegistration for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync + Default, + { + fn get_type_registration() -> $crate::type_registry::TypeRegistration { + let mut registration = $crate::type_registry::TypeRegistration::of::(); + registration.insert::<$crate::type_registry::ReflectFromPtr>($crate::type_registry::FromType::::from_type()); + registration.insert::<$crate::from_reflect::ReflectFromReflect>($crate::type_registry::FromType::::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut $crate::type_registry::TypeRegistry) { + registry.register::(); + registry.register::(); + } + } + + impl $crate::from_reflect::FromReflect for $ty + where + K: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + V: $crate::from_reflect::FromReflect + $crate::type_info::MaybeTyped + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn from_reflect(reflect: &dyn $crate::reflect::PartialReflect) -> Option { + let ref_map = reflect.reflect_ref().as_map().ok()?; + + let mut new_map = Self::with_capacity_and_hasher(ref_map.len(), S::default()); + + for (key, value) in ref_map.iter() { + let new_key = K::from_reflect(key)?; + let new_value = V::from_reflect(value)?; + new_map.insert(new_key, new_value); + } + + Some(new_map) + } + } + }; + }; +} + +pub(crate) use impl_reflect_for_hashmap; diff --git a/crates/bevy_reflect/src/impls/macros/mod.rs b/crates/bevy_reflect/src/impls/macros/mod.rs new file mode 100644 index 0000000000..1e57c39626 --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/mod.rs @@ -0,0 +1,7 @@ +pub(crate) use list::*; +pub(crate) use map::*; +pub(crate) use set::*; + +mod list; +mod map; +mod set; diff --git a/crates/bevy_reflect/src/impls/macros/set.rs b/crates/bevy_reflect/src/impls/macros/set.rs new file mode 100644 index 0000000000..e00e764e17 --- /dev/null +++ b/crates/bevy_reflect/src/impls/macros/set.rs @@ -0,0 +1,208 @@ +macro_rules! impl_reflect_for_hashset { + ($ty:path) => { + const _: () = { + impl $crate::set::Set for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get(&self, value: &dyn $crate::reflect::PartialReflect) -> Option<&dyn $crate::reflect::PartialReflect> { + value + .try_downcast_ref::() + .and_then(|value| Self::get(self, value)) + .map(|value| value as &dyn $crate::reflect::PartialReflect) + } + + fn len(&self) -> usize { + Self::len(self) + } + + fn iter(&self) -> bevy_platform::prelude::Box + '_> { + let iter = self.iter().map(|v| v as &dyn $crate::reflect::PartialReflect); + bevy_platform::prelude::Box::new(iter) + } + + fn drain(&mut self) -> bevy_platform::prelude::Vec> { + self.drain() + .map(|value| bevy_platform::prelude::Box::new(value) as bevy_platform::prelude::Box) + .collect() + } + + fn insert_boxed(&mut self, value: bevy_platform::prelude::Box) -> bool { + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(value) + } + + fn remove(&mut self, value: &dyn $crate::reflect::PartialReflect) -> bool { + let mut from_reflect = None; + value + .try_downcast_ref::() + .or_else(|| { + from_reflect = V::from_reflect(value); + from_reflect.as_ref() + }) + .is_some_and(|value| self.remove(value)) + } + + fn contains(&self, value: &dyn $crate::reflect::PartialReflect) -> bool { + let mut from_reflect = None; + value + .try_downcast_ref::() + .or_else(|| { + from_reflect = V::from_reflect(value); + from_reflect.as_ref() + }) + .is_some_and(|value| self.contains(value)) + } + } + + impl $crate::reflect::PartialReflect for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn get_represented_type_info(&self) -> Option<&'static $crate::type_info::TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { + self + } + + fn as_partial_reflect(&self) -> &dyn $crate::reflect::PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn $crate::reflect::PartialReflect { + self + } + + #[inline] + fn try_into_reflect( + self: bevy_platform::prelude::Box, + ) -> Result, bevy_platform::prelude::Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn $crate::reflect::Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn $crate::reflect::Reflect> { + Some(self) + } + + fn apply(&mut self, value: &dyn $crate::reflect::PartialReflect) { + $crate::set::set_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn $crate::reflect::PartialReflect) -> Result<(), $crate::reflect::ApplyError> { + $crate::set::set_try_apply(self, value) + } + + fn reflect_kind(&self) -> $crate::kind::ReflectKind { + $crate::kind::ReflectKind::Set + } + + fn reflect_ref(&self) -> $crate::kind::ReflectRef { + $crate::kind::ReflectRef::Set(self) + } + + fn reflect_mut(&mut self) -> $crate::kind::ReflectMut { + $crate::kind::ReflectMut::Set(self) + } + + fn reflect_owned(self: bevy_platform::prelude::Box) -> $crate::kind::ReflectOwned { + $crate::kind::ReflectOwned::Set(self) + } + + fn reflect_clone(&self) -> Result, $crate::error::ReflectCloneError> { + let mut set = Self::with_capacity_and_hasher(self.len(), S::default()); + for value in self.iter() { + let value = value.reflect_clone()?.take().map_err(|_| { + $crate::error::ReflectCloneError::FailedDowncast { + expected: alloc::borrow::Cow::Borrowed(::type_path()), + received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(value.reflect_type_path())), + } + })?; + set.insert(value); + } + + Ok(bevy_platform::prelude::Box::new(set)) + } + + fn reflect_partial_eq(&self, value: &dyn $crate::reflect::PartialReflect) -> Option { + $crate::set::set_partial_eq(self, value) + } + } + + impl $crate::type_info::Typed for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn type_info() -> &'static $crate::type_info::TypeInfo { + static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| { + $crate::type_info::TypeInfo::Set( + $crate::set::SetInfo::new::().with_generics($crate::generics::Generics::from_iter([ + $crate::generics::TypeParamInfo::new::("V") + ])) + ) + }) + } + } + + impl $crate::type_registry::GetTypeRegistration for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync + Default, + { + fn get_type_registration() -> $crate::type_registry::TypeRegistration { + let mut registration = $crate::type_registry::TypeRegistration::of::(); + registration.insert::<$crate::type_registry::ReflectFromPtr>($crate::type_registry::FromType::::from_type()); + registration.insert::<$crate::from_reflect::ReflectFromReflect>($crate::type_registry::FromType::::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut $crate::type_registry::TypeRegistry) { + registry.register::(); + } + } + + $crate::impl_full_reflect!( + for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + ); + + impl $crate::from_reflect::FromReflect for $ty + where + V: $crate::from_reflect::FromReflect + $crate::type_path::TypePath + $crate::type_registry::GetTypeRegistration + Eq + core::hash::Hash, + S: $crate::type_path::TypePath + core::hash::BuildHasher + Default + Send + Sync, + { + fn from_reflect(reflect: &dyn $crate::reflect::PartialReflect) -> Option { + let ref_set = reflect.reflect_ref().as_set().ok()?; + + let mut new_set = Self::with_capacity_and_hasher(ref_set.len(), S::default()); + + for value in ref_set.iter() { + let new_value = V::from_reflect(value)?; + new_set.insert(new_value); + } + + Some(new_set) + } + } + }; + }; +} + +pub(crate) use impl_reflect_for_hashset; diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 942bcbe83f..561111a901 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -77,6 +77,7 @@ where .collect() } } + impl PartialReflect for SmallVec where T::Item: FromReflect + MaybeTyped + TypePath, diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs deleted file mode 100644 index 0115afbf79..0000000000 --- a/crates/bevy_reflect/src/impls/std.rs +++ /dev/null @@ -1,2873 +0,0 @@ -#![expect( - unused_qualifications, - reason = "Temporary workaround for impl_reflect!(Option/Result false-positive" -)] - -use crate::{ - impl_type_path, map_apply, map_partial_eq, map_try_apply, - prelude::ReflectDefault, - reflect::impl_full_reflect, - set_apply, set_partial_eq, set_try_apply, - utility::{reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell}, - ApplyError, Array, ArrayInfo, ArrayIter, DynamicMap, DynamicTypePath, FromReflect, FromType, - Generics, GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, MaybeTyped, - OpaqueInfo, PartialReflect, Reflect, ReflectCloneError, ReflectDeserialize, ReflectFromPtr, - ReflectFromReflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, ReflectSerialize, Set, - SetInfo, TypeInfo, TypeParamInfo, TypePath, TypeRegistration, TypeRegistry, Typed, -}; -use alloc::{ - borrow::{Cow, ToOwned}, - boxed::Box, - collections::VecDeque, - format, - string::ToString, - vec::Vec, -}; -use bevy_reflect_derive::{impl_reflect, impl_reflect_opaque}; -use core::{ - any::Any, - fmt, - hash::{BuildHasher, Hash, Hasher}, - panic::Location, -}; - -#[cfg(feature = "std")] -use std::path::Path; - -impl_reflect_opaque!(bool( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(char( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(u128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(usize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(i128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(isize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(f32( - Clone, - Debug, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(f64( - Clone, - Debug, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_type_path!(str); -impl_reflect_opaque!(::alloc::string::String( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -#[cfg(feature = "std")] -impl_reflect_opaque!(::std::path::PathBuf( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(::core::any::TypeId(Clone, Debug, Hash, PartialEq,)); -impl_reflect_opaque!(::alloc::collections::BTreeSet(Clone)); -impl_reflect_opaque!(::core::ops::Range(Clone)); -impl_reflect_opaque!(::core::ops::RangeInclusive(Clone)); -impl_reflect_opaque!(::core::ops::RangeFrom(Clone)); -impl_reflect_opaque!(::core::ops::RangeTo(Clone)); -impl_reflect_opaque!(::core::ops::RangeToInclusive(Clone)); -impl_reflect_opaque!(::core::ops::RangeFull(Clone)); -impl_reflect_opaque!(::core::ops::Bound(Clone)); -impl_reflect_opaque!(::core::time::Duration( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize, - Default -)); -impl_reflect_opaque!(::bevy_platform::time::Instant( - Clone, Debug, Hash, PartialEq -)); -impl_reflect_opaque!(::core::num::NonZeroI128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU128( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroIsize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroUsize( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU64( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI32( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU16( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroU8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::NonZeroI8( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -impl_reflect_opaque!(::core::num::Wrapping(Clone)); -impl_reflect_opaque!(::core::num::Saturating(Clone)); -impl_reflect_opaque!(::bevy_platform::sync::Arc(Clone)); - -// `Serialize` and `Deserialize` only for platforms supported by serde: -// https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732 -#[cfg(all(any(unix, windows), feature = "std"))] -impl_reflect_opaque!(::std::ffi::OsString( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); -#[cfg(all(not(any(unix, windows)), feature = "std"))] -impl_reflect_opaque!(::std::ffi::OsString(Clone, Debug, Hash, PartialEq)); -impl_reflect_opaque!(::alloc::collections::BinaryHeap(Clone)); - -macro_rules! impl_reflect_for_atomic { - ($ty:ty, $ordering:expr) => { - impl_type_path!($ty); - - const _: () = { - #[cfg(feature = "functions")] - crate::func::macros::impl_function_traits!($ty); - - impl GetTypeRegistration for $ty - where - $ty: Any + Send + Sync, - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - - // Serde only supports atomic types when the "std" feature is enabled - #[cfg(feature = "std")] - { - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - } - - registration - } - } - - impl Typed for $ty - where - $ty: Any + Send + Sync, - { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| { - let info = OpaqueInfo::new::(); - TypeInfo::Opaque(info) - }) - } - } - - impl PartialReflect for $ty - where - $ty: Any + Send + Sync, - { - #[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 - } - #[inline] - 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) - } - - #[inline] - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(<$ty>::new(self.load($ordering)))) - } - - #[inline] - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - *self = <$ty>::new(value.load($ordering)); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: Into::into(DynamicTypePath::reflect_type_path(value)), - to_type: Into::into(::type_path()), - }); - } - Ok(()) - } - #[inline] - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - #[inline] - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - #[inline] - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - #[inline] - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(self, f) - } - } - - impl FromReflect for $ty - where - $ty: Any + Send + Sync, - { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(<$ty>::new( - reflect.try_downcast_ref::<$ty>()?.load($ordering), - )) - } - } - }; - - impl_full_reflect!(for $ty where $ty: Any + Send + Sync); - }; -} - -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicIsize, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicUsize, - ::core::sync::atomic::Ordering::SeqCst -); -#[cfg(target_has_atomic = "64")] -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI64, - ::core::sync::atomic::Ordering::SeqCst -); -#[cfg(target_has_atomic = "64")] -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU64, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI32, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU32, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI16, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU16, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicI8, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicU8, - ::core::sync::atomic::Ordering::SeqCst -); -impl_reflect_for_atomic!( - ::core::sync::atomic::AtomicBool, - ::core::sync::atomic::Ordering::SeqCst -); - -macro_rules! impl_reflect_for_veclike { - ($ty:ty, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { - impl List for $ty { - #[inline] - fn get(&self, index: usize) -> Option<&dyn PartialReflect> { - <$sub>::get(self, index).map(|value| value as &dyn PartialReflect) - } - - #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - <$sub>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) - } - - fn insert(&mut self, index: usize, value: Box) { - let value = value.try_take::().unwrap_or_else(|value| { - T::from_reflect(&*value).unwrap_or_else(|| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }) - }); - $insert(self, index, value); - } - - fn remove(&mut self, index: usize) -> Box { - Box::new($remove(self, index)) - } - - fn push(&mut self, value: Box) { - let value = T::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to push invalid value of type {}.", - value.reflect_type_path() - ) - }); - $push(self, value); - } - - fn pop(&mut self) -> Option> { - $pop(self).map(|value| Box::new(value) as Box) - } - - #[inline] - fn len(&self) -> usize { - <$sub>::len(self) - } - - #[inline] - fn iter(&self) -> ListIter { - ListIter::new(self) - } - - #[inline] - fn drain(&mut self) -> Vec> { - self.drain(..) - .map(|value| Box::new(value) as Box) - .collect() - } - } - - impl PartialReflect for $ty { - #[inline] - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - 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) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::List - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::List(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::List(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::List(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new( - self.iter() - .map(|value| { - value.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - } - }) - }) - .collect::>()?, - )) - } - - fn reflect_hash(&self) -> Option { - crate::list_hash(self) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - crate::list_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - crate::list_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - crate::list_try_apply(self, value) - } - } - - impl_full_reflect!( for $ty where T: FromReflect + MaybeTyped + TypePath + GetTypeRegistration); - - impl Typed for $ty { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::List( - ListInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("T") - ])) - ) - }) - } - } - - impl GetTypeRegistration - for $ty - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::<$ty>(); - registration.insert::(FromType::<$ty>::from_type()); - registration.insert::(FromType::<$ty>::from_type()); - registration - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } - } - - impl FromReflect for $ty { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_list = reflect.reflect_ref().as_list().ok()?; - - let mut new_list = Self::with_capacity(ref_list.len()); - - for field in ref_list.iter() { - $push(&mut new_list, T::from_reflect(field)?); - } - - Some(new_list) - } - } - }; -} - -impl_reflect_for_veclike!(Vec, Vec::insert, Vec::remove, Vec::push, Vec::pop, [T]); -impl_type_path!(::alloc::vec::Vec); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(Vec; ); - -impl_reflect_for_veclike!( - VecDeque, - VecDeque::insert, - VecDeque::remove, - VecDeque::push_back, - VecDeque::pop_back, - VecDeque:: -); -impl_type_path!(::alloc::collections::VecDeque); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(VecDeque; ); - -macro_rules! impl_reflect_for_hashmap { - ($ty:path) => { - impl Map for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(|key| Self::get(self, key)) - .map(|value| value as &dyn PartialReflect) - } - - fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(move |key| Self::get_mut(self, key)) - .map(|value| value as &mut dyn PartialReflect) - } - - fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { - self.iter() - .nth(index) - .map(|(key, value)| (key as &dyn PartialReflect, value as &dyn PartialReflect)) - } - - fn get_at_mut( - &mut self, - index: usize, - ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { - self.iter_mut().nth(index).map(|(key, value)| { - (key as &dyn PartialReflect, value as &mut dyn PartialReflect) - }) - } - - fn len(&self) -> usize { - Self::len(self) - } - - fn iter(&self) -> MapIter { - MapIter::new(self) - } - - fn drain(&mut self) -> Vec<(Box, Box)> { - self.drain() - .map(|(key, value)| { - ( - Box::new(key) as Box, - Box::new(value) as Box, - ) - }) - .collect() - } - - fn to_dynamic_map(&self) -> DynamicMap { - let mut dynamic_map = DynamicMap::default(); - dynamic_map.set_represented_type(self.get_represented_type_info()); - for (k, v) in self { - let key = K::from_reflect(k).unwrap_or_else(|| { - panic!( - "Attempted to clone invalid key of type {}.", - k.reflect_type_path() - ) - }); - dynamic_map.insert_boxed(Box::new(key), v.to_dynamic()); - } - dynamic_map - } - - fn insert_boxed( - &mut self, - key: Box, - value: Box, - ) -> Option> { - let key = K::take_from_reflect(key).unwrap_or_else(|key| { - panic!( - "Attempted to insert invalid key of type {}.", - key.reflect_type_path() - ) - }); - let value = V::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.insert(key, value) - .map(|old_value| Box::new(old_value) as Box) - } - - fn remove(&mut self, key: &dyn PartialReflect) -> Option> { - let mut from_reflect = None; - key.try_downcast_ref::() - .or_else(|| { - from_reflect = K::from_reflect(key); - from_reflect.as_ref() - }) - .and_then(|key| self.remove(key)) - .map(|value| Box::new(value) as Box) - } - } - - impl PartialReflect for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect( - self: Box, - ) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Map - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Map(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Map(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Map(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - let mut map = Self::with_capacity_and_hasher(self.len(), S::default()); - for (key, value) in self.iter() { - let key = key.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(key.reflect_type_path().to_string()), - } - })?; - let value = value.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - } - })?; - map.insert(key, value); - } - - Ok(Box::new(map)) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - map_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - map_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - map_try_apply(self, value) - } - } - - impl_full_reflect!( - for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - ); - - impl Typed for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::Map( - MapInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("K"), - TypeParamInfo::new::("V"), - ])), - ) - }) - } - } - - impl GetTypeRegistration for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync + Default, - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - registry.register::(); - } - } - - impl FromReflect for $ty - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_map = reflect.reflect_ref().as_map().ok()?; - - let mut new_map = Self::with_capacity_and_hasher(ref_map.len(), S::default()); - - for (key, value) in ref_map.iter() { - let new_key = K::from_reflect(key)?; - let new_value = V::from_reflect(value)?; - new_map.insert(new_key, new_value); - } - - Some(new_map) - } - } - }; -} - -#[cfg(feature = "std")] -impl_reflect_for_hashmap!(::std::collections::HashMap); -impl_type_path!(::core::hash::BuildHasherDefault); -#[cfg(feature = "std")] -impl_type_path!(::std::collections::hash_map::RandomState); -#[cfg(feature = "std")] -impl_type_path!(::std::collections::HashMap); -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(::std::collections::HashMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -impl_reflect_for_hashmap!(bevy_platform::collections::HashMap); -impl_type_path!(::bevy_platform::collections::HashMap); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -#[cfg(feature = "hashbrown")] -impl_reflect_for_hashmap!(hashbrown::hash_map::HashMap); -#[cfg(feature = "hashbrown")] -impl_type_path!(::hashbrown::hash_map::HashMap); -#[cfg(all(feature = "functions", feature = "hashbrown"))] -crate::func::macros::impl_function_traits!(::hashbrown::hash_map::HashMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -macro_rules! impl_reflect_for_hashset { - ($ty:path) => { - impl Set for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get(&self, value: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - value - .try_downcast_ref::() - .and_then(|value| Self::get(self, value)) - .map(|value| value as &dyn PartialReflect) - } - - fn len(&self) -> usize { - Self::len(self) - } - - fn iter(&self) -> Box + '_> { - let iter = self.iter().map(|v| v as &dyn PartialReflect); - Box::new(iter) - } - - fn drain(&mut self) -> Vec> { - self.drain() - .map(|value| Box::new(value) as Box) - .collect() - } - - fn insert_boxed(&mut self, value: Box) -> bool { - let value = V::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.insert(value) - } - - fn remove(&mut self, value: &dyn PartialReflect) -> bool { - let mut from_reflect = None; - value - .try_downcast_ref::() - .or_else(|| { - from_reflect = V::from_reflect(value); - from_reflect.as_ref() - }) - .is_some_and(|value| self.remove(value)) - } - - fn contains(&self, value: &dyn PartialReflect) -> bool { - let mut from_reflect = None; - value - .try_downcast_ref::() - .or_else(|| { - from_reflect = V::from_reflect(value); - from_reflect.as_ref() - }) - .is_some_and(|value| self.contains(value)) - } - } - - impl PartialReflect for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - #[inline] - fn try_into_reflect( - self: Box, - ) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - set_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - set_try_apply(self, value) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Set - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Set(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Set(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Set(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - let mut set = Self::with_capacity_and_hasher(self.len(), S::default()); - for value in self.iter() { - let value = value.reflect_clone()?.take().map_err(|_| { - ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - } - })?; - set.insert(value); - } - - Ok(Box::new(set)) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - set_partial_eq(self, value) - } - } - - impl Typed for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::Set( - SetInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("V") - ])) - ) - }) - } - } - - impl GetTypeRegistration for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync + Default, - { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } - } - - impl_full_reflect!( - for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - ); - - impl FromReflect for $ty - where - V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - S: TypePath + BuildHasher + Default + Send + Sync, - { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_set = reflect.reflect_ref().as_set().ok()?; - - let mut new_set = Self::with_capacity_and_hasher(ref_set.len(), S::default()); - - for value in ref_set.iter() { - let new_value = V::from_reflect(value)?; - new_set.insert(new_value); - } - - Some(new_set) - } - } - }; -} - -impl_type_path!(::bevy_platform::hash::NoOpHash); -impl_type_path!(::bevy_platform::hash::FixedHasher); -impl_type_path!(::bevy_platform::hash::PassHash); -impl_reflect_opaque!(::core::net::SocketAddr( - Clone, - Debug, - Hash, - PartialEq, - Serialize, - Deserialize -)); - -#[cfg(feature = "std")] -impl_reflect_for_hashset!(::std::collections::HashSet); -#[cfg(feature = "std")] -impl_type_path!(::std::collections::HashSet); -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(::std::collections::HashSet; - < - V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -impl_reflect_for_hashset!(::bevy_platform::collections::HashSet); -impl_type_path!(::bevy_platform::collections::HashSet); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashSet; - < - V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -#[cfg(feature = "hashbrown")] -impl_reflect_for_hashset!(::hashbrown::hash_set::HashSet); -#[cfg(feature = "hashbrown")] -impl_type_path!(::hashbrown::hash_set::HashSet); -#[cfg(all(feature = "functions", feature = "hashbrown"))] -crate::func::macros::impl_function_traits!(::hashbrown::hash_set::HashSet; - < - V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, - S: TypePath + BuildHasher + Default + Send + Sync - > -); - -impl Map for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(|key| Self::get(self, key)) - .map(|value| value as &dyn PartialReflect) - } - - fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { - key.try_downcast_ref::() - .and_then(move |key| Self::get_mut(self, key)) - .map(|value| value as &mut dyn PartialReflect) - } - - fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { - self.iter() - .nth(index) - .map(|(key, value)| (key as &dyn PartialReflect, value as &dyn PartialReflect)) - } - - fn get_at_mut( - &mut self, - index: usize, - ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { - self.iter_mut() - .nth(index) - .map(|(key, value)| (key as &dyn PartialReflect, value as &mut dyn PartialReflect)) - } - - fn len(&self) -> usize { - Self::len(self) - } - - fn iter(&self) -> MapIter { - MapIter::new(self) - } - - fn drain(&mut self) -> Vec<(Box, Box)> { - // BTreeMap doesn't have a `drain` function. See - // https://github.com/rust-lang/rust/issues/81074. So we have to fake one by popping - // elements off one at a time. - let mut result = Vec::with_capacity(self.len()); - while let Some((k, v)) = self.pop_first() { - result.push(( - Box::new(k) as Box, - Box::new(v) as Box, - )); - } - result - } - - fn insert_boxed( - &mut self, - key: Box, - value: Box, - ) -> Option> { - let key = K::take_from_reflect(key).unwrap_or_else(|key| { - panic!( - "Attempted to insert invalid key of type {}.", - key.reflect_type_path() - ) - }); - let value = V::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.insert(key, value) - .map(|old_value| Box::new(old_value) as Box) - } - - fn remove(&mut self, key: &dyn PartialReflect) -> Option> { - let mut from_reflect = None; - key.try_downcast_ref::() - .or_else(|| { - from_reflect = K::from_reflect(key); - from_reflect.as_ref() - }) - .and_then(|key| self.remove(key)) - .map(|value| Box::new(value) as Box) - } -} - -impl PartialReflect for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - #[inline] - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Map - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Map(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Map(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Map(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - let mut map = Self::new(); - for (key, value) in self.iter() { - let key = - key.reflect_clone()? - .take() - .map_err(|_| ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(key.reflect_type_path().to_string()), - })?; - let value = - value - .reflect_clone()? - .take() - .map_err(|_| ReflectCloneError::FailedDowncast { - expected: Cow::Borrowed(::type_path()), - received: Cow::Owned(value.reflect_type_path().to_string()), - })?; - map.insert(key, value); - } - - Ok(Box::new(map)) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - map_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - map_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - map_try_apply(self, value) - } -} - -impl_full_reflect!( - for ::alloc::collections::BTreeMap - where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -); - -impl Typed for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| { - TypeInfo::Map( - MapInfo::new::().with_generics(Generics::from_iter([ - TypeParamInfo::new::("K"), - TypeParamInfo::new::("V"), - ])), - ) - }) - } -} - -impl GetTypeRegistration for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for ::alloc::collections::BTreeMap -where - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, -{ - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_map = reflect.reflect_ref().as_map().ok()?; - - let mut new_map = Self::new(); - - for (key, value) in ref_map.iter() { - let new_key = K::from_reflect(key)?; - let new_value = V::from_reflect(value)?; - new_map.insert(new_key, new_value); - } - - Some(new_map) - } -} - -impl_type_path!(::alloc::collections::BTreeMap); -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(::alloc::collections::BTreeMap; - < - K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration - > -); - -impl Array for [T; N] { - #[inline] - fn get(&self, index: usize) -> Option<&dyn PartialReflect> { - <[T]>::get(self, index).map(|value| value as &dyn PartialReflect) - } - - #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - <[T]>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) - } - - #[inline] - fn len(&self) -> usize { - N - } - - #[inline] - fn iter(&self) -> ArrayIter { - ArrayIter::new(self) - } - - #[inline] - fn drain(self: Box) -> Vec> { - self.into_iter() - .map(|value| Box::new(value) as Box) - .collect() - } -} - -impl PartialReflect - for [T; N] -{ - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - #[inline] - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Array - } - - #[inline] - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Array(self) - } - - #[inline] - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Array(self) - } - - #[inline] - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Array(self) - } - - #[inline] - fn reflect_hash(&self) -> Option { - crate::array_hash(self) - } - - #[inline] - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - crate::array_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - crate::array_apply(self, value); - } - - #[inline] - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - crate::array_try_apply(self, value) - } -} - -impl Reflect for [T; N] { - #[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(()) - } -} - -impl FromReflect - for [T; N] -{ - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_array = reflect.reflect_ref().as_array().ok()?; - - let mut temp_vec = Vec::with_capacity(ref_array.len()); - - for field in ref_array.iter() { - temp_vec.push(T::from_reflect(field)?); - } - - temp_vec.try_into().ok() - } -} - -impl Typed for [T; N] { - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| TypeInfo::Array(ArrayInfo::new::(N))) - } -} - -impl TypePath for [T; N] { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{t}; {N}]", t = T::short_type_path())) - } -} - -impl GetTypeRegistration - for [T; N] -{ - fn get_type_registration() -> TypeRegistration { - TypeRegistration::of::<[T; N]>() - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!([T; N]; [const N: usize]); - -impl_reflect! { - #[type_path = "core::option"] - enum Option { - None, - Some(T), - } -} - -impl_reflect! { - #[type_path = "core::result"] - enum Result { - Ok(T), - Err(E), - } -} - -impl TypePath for &'static T { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&{}", T::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&{}", T::short_type_path())) - } -} - -impl TypePath for &'static mut T { - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&mut {}", T::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("&mut {}", T::short_type_path())) - } -} - -impl PartialReflect for Cow<'static, str> { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(self.clone())) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { - fmt::Debug::fmt(self, f) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static - to_type: Self::type_path().into(), - }); - } - Ok(()) - } -} - -impl_full_reflect!(for Cow<'static, str>); - -impl Typed for Cow<'static, str> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -impl GetTypeRegistration for Cow<'static, str> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::>(); - registration.insert::(FromType::>::from_type()); - registration.insert::(FromType::>::from_type()); - registration.insert::(FromType::>::from_type()); - registration.insert::(FromType::>::from_type()); - registration - } -} - -impl FromReflect for Cow<'static, str> { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::>()?.clone()) - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(Cow<'static, str>); - -impl TypePath for [T] -where - [T]: ToOwned, -{ - fn type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{}]", ::type_path())) - } - - fn short_type_path() -> &'static str { - static CELL: GenericTypePathCell = GenericTypePathCell::new(); - CELL.get_or_insert::(|| format!("[{}]", ::short_type_path())) - } -} - -impl List - for Cow<'static, [T]> -{ - fn get(&self, index: usize) -> Option<&dyn PartialReflect> { - self.as_ref().get(index).map(|x| x as &dyn PartialReflect) - } - - fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { - self.to_mut() - .get_mut(index) - .map(|x| x as &mut dyn PartialReflect) - } - - fn insert(&mut self, index: usize, element: Box) { - let value = T::take_from_reflect(element).unwrap_or_else(|value| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ); - }); - self.to_mut().insert(index, value); - } - - fn remove(&mut self, index: usize) -> Box { - Box::new(self.to_mut().remove(index)) - } - - fn push(&mut self, value: Box) { - let value = T::take_from_reflect(value).unwrap_or_else(|value| { - panic!( - "Attempted to push invalid value of type {}.", - value.reflect_type_path() - ) - }); - self.to_mut().push(value); - } - - fn pop(&mut self) -> Option> { - self.to_mut() - .pop() - .map(|value| Box::new(value) as Box) - } - - fn len(&self) -> usize { - self.as_ref().len() - } - - fn iter(&self) -> ListIter { - ListIter::new(self) - } - - fn drain(&mut self) -> Vec> { - self.to_mut() - .drain(..) - .map(|value| Box::new(value) as Box) - .collect() - } -} - -impl PartialReflect - for Cow<'static, [T]> -{ - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::List - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::List(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::List(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::List(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(self.clone())) - } - - fn reflect_hash(&self) -> Option { - crate::list_hash(self) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - crate::list_partial_eq(self, value) - } - - fn apply(&mut self, value: &dyn PartialReflect) { - crate::list_apply(self, value); - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - crate::list_try_apply(self, value) - } -} - -impl_full_reflect!( - for Cow<'static, [T]> - where - T: FromReflect + Clone + MaybeTyped + TypePath + GetTypeRegistration, -); - -impl Typed - for Cow<'static, [T]> -{ - fn type_info() -> &'static TypeInfo { - static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); - CELL.get_or_insert::(|| TypeInfo::List(ListInfo::new::())) - } -} - -impl GetTypeRegistration - for Cow<'static, [T]> -{ - fn get_type_registration() -> TypeRegistration { - TypeRegistration::of::>() - } - - fn register_type_dependencies(registry: &mut TypeRegistry) { - registry.register::(); - } -} - -impl FromReflect - for Cow<'static, [T]> -{ - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - let ref_list = reflect.reflect_ref().as_list().ok()?; - - let mut temp_vec = Vec::with_capacity(ref_list.len()); - - for field in ref_list.iter() { - temp_vec.push(T::from_reflect(field)?); - } - - Some(temp_vec.into()) - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(Cow<'static, [T]>; ); - -impl PartialReflect for &'static str { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(*self)) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self, f) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: Self::type_path().into(), - }); - } - Ok(()) - } -} - -impl Reflect for &'static str { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -impl Typed for &'static str { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -impl GetTypeRegistration for &'static str { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for &'static str { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - reflect.try_downcast_ref::().copied() - } -} - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(&'static str); - -#[cfg(feature = "std")] -impl PartialReflect for &'static Path { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(*self)) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } -} - -#[cfg(feature = "std")] -impl Reflect for &'static Path { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -#[cfg(feature = "std")] -impl Typed for &'static Path { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -#[cfg(feature = "std")] -impl GetTypeRegistration for &'static Path { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -#[cfg(feature = "std")] -impl FromReflect for &'static Path { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - reflect.try_downcast_ref::().copied() - } -} - -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(&'static Path); - -#[cfg(feature = "std")] -impl PartialReflect for Cow<'static, Path> { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(self.clone())) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self, f) - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } -} - -#[cfg(feature = "std")] -impl Reflect for Cow<'static, Path> { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -#[cfg(feature = "std")] -impl Typed for Cow<'static, Path> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -#[cfg(feature = "std")] -impl_type_path!(::std::path::Path); -impl_type_path!(::alloc::borrow::Cow<'a: 'static, T: ToOwned + ?Sized>); - -#[cfg(feature = "std")] -impl FromReflect for Cow<'static, Path> { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - Some(reflect.try_downcast_ref::()?.clone()) - } -} - -#[cfg(feature = "std")] -impl GetTypeRegistration for Cow<'static, Path> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(Cow<'static, Path>); - -impl TypePath for &'static Location<'static> { - fn type_path() -> &'static str { - "core::panic::Location" - } - - fn short_type_path() -> &'static str { - "Location" - } -} - -impl PartialReflect for &'static Location<'static> { - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } - - #[inline] - fn into_partial_reflect(self: Box) -> Box { - self - } - - fn as_partial_reflect(&self) -> &dyn PartialReflect { - self - } - - fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { - self - } - - fn try_into_reflect(self: Box) -> Result, Box> { - Ok(self) - } - - fn try_as_reflect(&self) -> Option<&dyn Reflect> { - Some(self) - } - - fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { - Some(self) - } - - fn reflect_kind(&self) -> ReflectKind { - ReflectKind::Opaque - } - - fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Opaque(self) - } - - fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Opaque(self) - } - - fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Opaque(self) - } - - fn reflect_clone(&self) -> Result, ReflectCloneError> { - Ok(Box::new(*self)) - } - - fn reflect_hash(&self) -> Option { - let mut hasher = reflect_hasher(); - Hash::hash(&Any::type_id(self), &mut hasher); - Hash::hash(self, &mut hasher); - Some(hasher.finish()) - } - - fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { - if let Some(value) = value.try_downcast_ref::() { - Some(PartialEq::eq(self, value)) - } else { - Some(false) - } - } - - fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let Some(value) = value.try_downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } -} - -impl Reflect for &'static Location<'static> { - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { - self - } - - fn as_reflect(&self) -> &dyn Reflect { - self - } - - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } -} - -impl Typed for &'static Location<'static> { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) - } -} - -impl GetTypeRegistration for &'static Location<'static> { - fn get_type_registration() -> TypeRegistration { - let mut registration = TypeRegistration::of::(); - registration.insert::(FromType::::from_type()); - registration.insert::(FromType::::from_type()); - registration - } -} - -impl FromReflect for &'static Location<'static> { - fn from_reflect(reflect: &dyn PartialReflect) -> Option { - reflect.try_downcast_ref::().copied() - } -} - -#[cfg(all(feature = "functions", feature = "std"))] -crate::func::macros::impl_function_traits!(&'static Location<'static>); - -#[cfg(test)] -mod tests { - use crate::{ - Enum, FromReflect, PartialReflect, Reflect, ReflectSerialize, TypeInfo, TypeRegistry, - Typed, VariantInfo, VariantType, - }; - use alloc::{collections::BTreeMap, string::String, vec}; - use bevy_platform::collections::HashMap; - use bevy_platform::time::Instant; - use core::{ - f32::consts::{PI, TAU}, - time::Duration, - }; - use static_assertions::assert_impl_all; - use std::path::Path; - - #[test] - fn can_serialize_duration() { - let mut type_registry = TypeRegistry::default(); - type_registry.register::(); - - let reflect_serialize = type_registry - .get_type_data::(core::any::TypeId::of::()) - .unwrap(); - let _serializable = reflect_serialize.get_serializable(&Duration::ZERO); - } - - #[test] - fn should_partial_eq_char() { - let a: &dyn PartialReflect = &'x'; - let b: &dyn PartialReflect = &'x'; - let c: &dyn PartialReflect = &'o'; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_i32() { - let a: &dyn PartialReflect = &123_i32; - let b: &dyn PartialReflect = &123_i32; - let c: &dyn PartialReflect = &321_i32; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_f32() { - let a: &dyn PartialReflect = &PI; - let b: &dyn PartialReflect = &PI; - let c: &dyn PartialReflect = &TAU; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_string() { - let a: &dyn PartialReflect = &String::from("Hello"); - let b: &dyn PartialReflect = &String::from("Hello"); - let c: &dyn PartialReflect = &String::from("World"); - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_vec() { - let a: &dyn PartialReflect = &vec![1, 2, 3]; - let b: &dyn PartialReflect = &vec![1, 2, 3]; - let c: &dyn PartialReflect = &vec![3, 2, 1]; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_hash_map() { - let mut a = >::default(); - a.insert(0usize, 1.23_f64); - let b = a.clone(); - let mut c = >::default(); - c.insert(0usize, 3.21_f64); - - let a: &dyn PartialReflect = &a; - let b: &dyn PartialReflect = &b; - let c: &dyn PartialReflect = &c; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); - } - - #[test] - fn should_partial_eq_btree_map() { - let mut a = BTreeMap::new(); - a.insert(0usize, 1.23_f64); - let b = a.clone(); - let mut c = BTreeMap::new(); - c.insert(0usize, 3.21_f64); - - let a: &dyn Reflect = &a; - let b: &dyn Reflect = &b; - let c: &dyn Reflect = &c; - assert!(a - .reflect_partial_eq(b.as_partial_reflect()) - .unwrap_or_default()); - assert!(!a - .reflect_partial_eq(c.as_partial_reflect()) - .unwrap_or_default()); - } - - #[test] - fn should_partial_eq_option() { - let a: &dyn PartialReflect = &Some(123); - let b: &dyn PartialReflect = &Some(123); - assert_eq!(Some(true), a.reflect_partial_eq(b)); - } - - #[test] - fn option_should_impl_enum() { - assert_impl_all!(Option<()>: Enum); - - let mut value = Some(123usize); - - assert!(value - .reflect_partial_eq(&Some(123usize)) - .unwrap_or_default()); - assert!(!value - .reflect_partial_eq(&Some(321usize)) - .unwrap_or_default()); - - assert_eq!("Some", value.variant_name()); - assert_eq!("core::option::Option::Some", value.variant_path()); - - if value.is_variant(VariantType::Tuple) { - if let Some(field) = value - .field_at_mut(0) - .and_then(|field| field.try_downcast_mut::()) - { - *field = 321; - } - } else { - panic!("expected `VariantType::Tuple`"); - } - - assert_eq!(Some(321), value); - } - - #[test] - fn option_should_from_reflect() { - #[derive(Reflect, PartialEq, Debug)] - struct Foo(usize); - - let expected = Some(Foo(123)); - let output = as FromReflect>::from_reflect(&expected).unwrap(); - - assert_eq!(expected, output); - } - - #[test] - fn option_should_apply() { - #[derive(Reflect, PartialEq, Debug)] - struct Foo(usize); - - // === None on None === // - let patch = None::; - let mut value = None::; - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "None apply onto None"); - - // === Some on None === // - let patch = Some(Foo(123)); - let mut value = None::; - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "Some apply onto None"); - - // === None on Some === // - let patch = None::; - let mut value = Some(Foo(321)); - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "None apply onto Some"); - - // === Some on Some === // - let patch = Some(Foo(123)); - let mut value = Some(Foo(321)); - PartialReflect::apply(&mut value, &patch); - - assert_eq!(patch, value, "Some apply onto Some"); - } - - #[test] - fn option_should_impl_typed() { - assert_impl_all!(Option<()>: Typed); - - type MyOption = Option; - let info = MyOption::type_info(); - if let TypeInfo::Enum(info) = info { - assert_eq!( - "None", - info.variant_at(0).unwrap().name(), - "Expected `None` to be variant at index `0`" - ); - assert_eq!( - "Some", - info.variant_at(1).unwrap().name(), - "Expected `Some` to be variant at index `1`" - ); - assert_eq!("Some", info.variant("Some").unwrap().name()); - if let VariantInfo::Tuple(variant) = info.variant("Some").unwrap() { - assert!( - variant.field_at(0).unwrap().is::(), - "Expected `Some` variant to contain `i32`" - ); - assert!( - variant.field_at(1).is_none(), - "Expected `Some` variant to only contain 1 field" - ); - } else { - panic!("Expected `VariantInfo::Tuple`"); - } - } else { - panic!("Expected `TypeInfo::Enum`"); - } - } - - #[test] - fn nonzero_usize_impl_reflect_from_reflect() { - let a: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); - let b: &dyn PartialReflect = &core::num::NonZero::::new(42).unwrap(); - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - let forty_two: core::num::NonZero = FromReflect::from_reflect(a).unwrap(); - assert_eq!(forty_two, core::num::NonZero::::new(42).unwrap()); - } - - #[test] - fn instant_should_from_reflect() { - let expected = Instant::now(); - let output = ::from_reflect(&expected).unwrap(); - assert_eq!(expected, output); - } - - #[test] - fn path_should_from_reflect() { - let path = Path::new("hello_world.rs"); - let output = <&'static Path as FromReflect>::from_reflect(&path).unwrap(); - assert_eq!(path, output); - } - - #[test] - fn type_id_should_from_reflect() { - let type_id = core::any::TypeId::of::(); - let output = ::from_reflect(&type_id).unwrap(); - assert_eq!(type_id, output); - } - - #[test] - fn static_str_should_from_reflect() { - let expected = "Hello, World!"; - let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); - assert_eq!(expected, output); - } - - #[test] - fn should_reflect_hashmaps() { - assert_impl_all!(std::collections::HashMap: Reflect); - assert_impl_all!(bevy_platform::collections::HashMap: Reflect); - - // We specify `foldhash::fast::RandomState` directly here since without the `default-hasher` - // feature, hashbrown uses an empty enum to force users to specify their own - #[cfg(feature = "hashbrown")] - assert_impl_all!(hashbrown::HashMap: Reflect); - } -} diff --git a/crates/bevy_reflect/src/impls/std/collections/hash_map.rs b/crates/bevy_reflect/src/impls/std/collections/hash_map.rs new file mode 100644 index 0000000000..604c29f517 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/collections/hash_map.rs @@ -0,0 +1,34 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashmap; +#[cfg(feature = "functions")] +use crate::{ + from_reflect::FromReflect, type_info::MaybeTyped, type_path::TypePath, + type_registry::GetTypeRegistration, +}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashmap!(::std::collections::HashMap); +impl_type_path!(::std::collections::hash_map::RandomState); +impl_type_path!(::std::collections::HashMap); + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +#[cfg(test)] +mod tests { + use crate::Reflect; + use static_assertions::assert_impl_all; + + #[test] + fn should_reflect_hashmaps() { + assert_impl_all!(std::collections::HashMap: Reflect); + } +} diff --git a/crates/bevy_reflect/src/impls/std/collections/hash_set.rs b/crates/bevy_reflect/src/impls/std/collections/hash_set.rs new file mode 100644 index 0000000000..26d2dd47ca --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/collections/hash_set.rs @@ -0,0 +1,17 @@ +use bevy_reflect_derive::impl_type_path; + +use crate::impls::macros::impl_reflect_for_hashset; +#[cfg(feature = "functions")] +use crate::{from_reflect::FromReflect, type_path::TypePath, type_registry::GetTypeRegistration}; +#[cfg(feature = "functions")] +use core::hash::{BuildHasher, Hash}; + +impl_reflect_for_hashset!(::std::collections::HashSet); +impl_type_path!(::std::collections::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); diff --git a/crates/bevy_reflect/src/impls/std/collections/mod.rs b/crates/bevy_reflect/src/impls/std/collections/mod.rs new file mode 100644 index 0000000000..2bde6a0653 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/collections/mod.rs @@ -0,0 +1,2 @@ +mod hash_map; +mod hash_set; diff --git a/crates/bevy_reflect/src/impls/std/ffi.rs b/crates/bevy_reflect/src/impls/std/ffi.rs new file mode 100644 index 0000000000..92cacc3b66 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/ffi.rs @@ -0,0 +1,18 @@ +#[cfg(any(unix, windows))] +use crate::type_registry::{ReflectDeserialize, ReflectSerialize}; +use bevy_reflect_derive::impl_reflect_opaque; + +// `Serialize` and `Deserialize` only for platforms supported by serde: +// https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732 +#[cfg(any(unix, windows))] +impl_reflect_opaque!(::std::ffi::OsString( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize +)); + +#[cfg(not(any(unix, windows)))] +impl_reflect_opaque!(::std::ffi::OsString(Clone, Debug, Hash, PartialEq)); diff --git a/crates/bevy_reflect/src/impls/std/mod.rs b/crates/bevy_reflect/src/impls/std/mod.rs new file mode 100644 index 0000000000..b3586e4d7d --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/mod.rs @@ -0,0 +1,46 @@ +mod collections; +mod ffi; +mod path; + +#[cfg(test)] +mod tests { + use crate::{FromReflect, PartialReflect}; + use std::collections::HashMap; + use std::path::Path; + + #[test] + fn should_partial_eq_hash_map() { + let mut a = >::default(); + a.insert(0usize, 1.23_f64); + let b = a.clone(); + let mut c = >::default(); + c.insert(0usize, 3.21_f64); + + let a: &dyn PartialReflect = &a; + let b: &dyn PartialReflect = &b; + let c: &dyn PartialReflect = &c; + assert!(a.reflect_partial_eq(b).unwrap_or_default()); + assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + } + + #[test] + fn path_should_from_reflect() { + let path = Path::new("hello_world.rs"); + let output = <&'static Path as FromReflect>::from_reflect(&path).unwrap(); + assert_eq!(path, output); + } + + #[test] + fn type_id_should_from_reflect() { + let type_id = core::any::TypeId::of::(); + let output = ::from_reflect(&type_id).unwrap(); + assert_eq!(type_id, output); + } + + #[test] + fn static_str_should_from_reflect() { + let expected = "Hello, World!"; + let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); + assert_eq!(expected, output); + } +} diff --git a/crates/bevy_reflect/src/impls/std/path.rs b/crates/bevy_reflect/src/impls/std/path.rs new file mode 100644 index 0000000000..a73ee44141 --- /dev/null +++ b/crates/bevy_reflect/src/impls/std/path.rs @@ -0,0 +1,306 @@ +use crate::{ + error::ReflectCloneError, + kind::{ReflectKind, ReflectMut, ReflectOwned, ReflectRef}, + prelude::*, + reflect::ApplyError, + type_info::{OpaqueInfo, TypeInfo, Typed}, + type_path::DynamicTypePath, + type_registry::{ + FromType, GetTypeRegistration, ReflectDeserialize, ReflectFromPtr, ReflectSerialize, + TypeRegistration, + }, + utility::{reflect_hasher, NonGenericTypeInfoCell}, +}; +use alloc::borrow::Cow; +use bevy_platform::prelude::*; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; +use core::any::Any; +use core::fmt; +use core::hash::{Hash, Hasher}; +use std::path::Path; + +impl_reflect_opaque!(::std::path::PathBuf( + Clone, + Debug, + Hash, + PartialEq, + Serialize, + Deserialize, + Default +)); + +impl PartialReflect for &'static Path { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(*self)) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for &'static Path { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for &'static Path { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl GetTypeRegistration for &'static Path { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +impl FromReflect for &'static Path { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static Path); + +impl PartialReflect for Cow<'static, Path> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Opaque + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Opaque(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Opaque(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Opaque(self) + } + + fn reflect_clone(&self) -> Result, ReflectCloneError> { + Ok(Box::new(self.clone())) + } + + fn reflect_hash(&self) -> Option { + let mut hasher = reflect_hasher(); + Hash::hash(&Any::type_id(self), &mut hasher); + Hash::hash(self, &mut hasher); + Some(hasher.finish()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) + } else { + Some(false) + } + } + + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self, f) + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for Cow<'static, Path> { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + +impl Typed for Cow<'static, Path> { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Opaque(OpaqueInfo::new::())) + } +} + +impl_type_path!(::std::path::Path); + +impl FromReflect for Cow<'static, Path> { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::()?.clone()) + } +} + +impl GetTypeRegistration for Cow<'static, Path> { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } +} + +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, Path>); diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index 3eef10d0e5..8988b30aa5 100644 --- a/crates/bevy_reflect/src/kind.rs +++ b/crates/bevy_reflect/src/kind.rs @@ -134,7 +134,9 @@ macro_rules! impl_reflect_kind_conversions { #[derive(Debug, Error)] #[error("kind mismatch: expected {expected:?}, received {received:?}")] pub struct ReflectKindMismatchError { + /// Expected kind. pub expected: ReflectKind, + /// Received kind. pub received: ReflectKind, } @@ -176,18 +178,49 @@ macro_rules! impl_cast_method { /// /// ["kinds"]: ReflectKind pub enum ReflectRef<'a> { + /// An immutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a dyn Struct), + /// An immutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a dyn TupleStruct), + /// An immutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a dyn Tuple), + /// An immutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a dyn List), + /// An immutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a dyn Array), + /// An immutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a dyn Map), + /// An immutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a dyn Set), + /// An immutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a dyn Enum), + /// An immutable reference to a [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(&'a dyn Function), + /// An immutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a dyn PartialReflect), } + impl_reflect_kind_conversions!(ReflectRef<'_>); impl<'a> ReflectRef<'a> { @@ -211,18 +244,49 @@ impl<'a> ReflectRef<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectMut<'a> { + /// A mutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a mut dyn Struct), + /// A mutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a mut dyn TupleStruct), + /// A mutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a mut dyn Tuple), + /// A mutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a mut dyn List), + /// A mutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a mut dyn Array), + /// A mutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a mut dyn Map), + /// A mutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a mut dyn Set), + /// A mutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a mut dyn Enum), #[cfg(feature = "functions")] + /// A mutable reference to a [function-like] type. + /// + /// [function-like]: Function Function(&'a mut dyn Function), + /// A mutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a mut dyn PartialReflect), } + impl_reflect_kind_conversions!(ReflectMut<'_>); impl<'a> ReflectMut<'a> { @@ -246,18 +310,49 @@ impl<'a> ReflectMut<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectOwned { + /// An owned [struct-like] type. + /// + /// [struct-like]: Struct Struct(Box), + /// An owned [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(Box), + /// An owned [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(Box), + /// An owned [list-like] type. + /// + /// [list-like]: List List(Box), + /// An owned [array-like] type. + /// + /// [array-like]: Array Array(Box), + /// An owned [map-like] type. + /// + /// [map-like]: Map Map(Box), + /// An owned [set-like] type. + /// + /// [set-like]: Set Set(Box), + /// An owned [enum-like] type. + /// + /// [enum-like]: Enum Enum(Box), + /// An owned [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(Box), + /// An owned [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(Box), } + impl_reflect_kind_conversions!(ReflectOwned); impl ReflectOwned { diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 58e9b8714f..eabfdc0eac 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1,4 +1,3 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr( any(docsrs, docsrs_dep), expect( @@ -8,8 +7,8 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Reflection in Rust. @@ -521,7 +520,7 @@ //! and displaying it in error messages. //! //! [Reflection]: https://en.wikipedia.org/wiki/Reflective_programming -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ //! [limitations]: #limitations //! [`bevy_reflect`]: crate //! [introspection]: https://en.wikipedia.org/wiki/Type_introspection @@ -589,7 +588,14 @@ mod type_path; mod type_registry; mod impls { + mod alloc; + mod bevy_platform; + mod core; mod foldhash; + #[cfg(feature = "hashbrown")] + mod hashbrown; + mod macros; + #[cfg(feature = "std")] mod std; #[cfg(feature = "glam")] @@ -2601,7 +2607,7 @@ bevy_reflect::tests::Test { let foo = Foo { a: 1 }; let foo: &dyn Reflect = &foo; - assert_eq!("123", format!("{:?}", foo)); + assert_eq!("123", format!("{foo:?}")); } #[test] @@ -2854,7 +2860,7 @@ bevy_reflect::tests::Test { test_unknown_tuple_struct.insert(14); test_struct.insert("unknown_tuplestruct", test_unknown_tuple_struct); assert_eq!( - format!("{:?}", test_struct), + format!("{test_struct:?}"), "DynamicStruct(bevy_reflect::tests::TestStruct { \ tuple: DynamicTuple((0, 1)), \ tuple_struct: DynamicTupleStruct(bevy_reflect::tests::TestTupleStruct(8)), \ diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index 7e768b8f1b..4ecdb63275 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -191,8 +191,7 @@ impl DynamicList { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::List(_)), - "expected TypeInfo::List but received: {:?}", - represented_type + "expected TypeInfo::List but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index e96537e67d..20531569e8 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -192,6 +192,8 @@ impl MapInfo { impl_generic_info_methods!(generics); } +/// Used to produce an error message when an attempt is made to hash +/// a [`PartialReflect`] value that does not support hashing. #[macro_export] macro_rules! hash_error { ( $key:expr ) => {{ @@ -236,8 +238,7 @@ impl DynamicMap { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Map(_)), - "expected TypeInfo::Map but received: {:?}", - represented_type + "expected TypeInfo::Map but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/path/error.rs b/crates/bevy_reflect/src/path/error.rs index 0e900c8315..00188a4cc3 100644 --- a/crates/bevy_reflect/src/path/error.rs +++ b/crates/bevy_reflect/src/path/error.rs @@ -74,6 +74,7 @@ impl<'a> AccessError<'a> { self.offset.as_ref() } } + impl fmt::Display for AccessError<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let AccessError { @@ -126,4 +127,5 @@ impl fmt::Display for AccessError<'_> { } } } + impl core::error::Error for AccessError<'_> {} diff --git a/crates/bevy_reflect/src/path/mod.rs b/crates/bevy_reflect/src/path/mod.rs index a52bbb6aaa..f0434686ee 100644 --- a/crates/bevy_reflect/src/path/mod.rs +++ b/crates/bevy_reflect/src/path/mod.rs @@ -82,6 +82,7 @@ pub trait ReflectPath<'a>: Sized { }) } } + impl<'a> ReflectPath<'a> for &'a str { fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for (access, offset) in PathParser::new(self) { @@ -437,6 +438,7 @@ impl ParsedPath { Ok(Self(parts)) } } + impl<'a> ReflectPath<'a> for &'a ParsedPath { fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for OffsetAccess { access, offset } in &self.0 { @@ -454,11 +456,13 @@ impl<'a> ReflectPath<'a> for &'a ParsedPath { Ok(root) } } + impl From<[OffsetAccess; N]> for ParsedPath { fn from(value: [OffsetAccess; N]) -> Self { ParsedPath(value.to_vec()) } } + impl From>> for ParsedPath { fn from(value: Vec>) -> Self { ParsedPath( @@ -472,6 +476,7 @@ impl From>> for ParsedPath { ) } } + impl From<[Access<'static>; N]> for ParsedPath { fn from(value: [Access<'static>; N]) -> Self { value.to_vec().into() @@ -493,12 +498,14 @@ impl fmt::Display for ParsedPath { Ok(()) } } + impl core::ops::Index for ParsedPath { type Output = OffsetAccess; fn index(&self, index: usize) -> &Self::Output { &self.0[index] } } + impl core::ops::IndexMut for ParsedPath { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] diff --git a/crates/bevy_reflect/src/path/parse.rs b/crates/bevy_reflect/src/path/parse.rs index 2ab2939a30..be5856834a 100644 --- a/crates/bevy_reflect/src/path/parse.rs +++ b/crates/bevy_reflect/src/path/parse.rs @@ -38,6 +38,7 @@ pub(super) struct PathParser<'a> { path: &'a str, remaining: &'a [u8], } + impl<'a> PathParser<'a> { pub(super) fn new(path: &'a str) -> Self { let remaining = path.as_bytes(); @@ -103,6 +104,7 @@ impl<'a> PathParser<'a> { self.path.len() - self.remaining.len() } } + impl<'a> Iterator for PathParser<'a> { type Item = (Result, ReflectPathError<'a>>, usize); @@ -149,6 +151,7 @@ enum Token<'a> { CloseBracket = b']', Ident(Ident<'a>), } + impl fmt::Display for Token<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -160,6 +163,7 @@ impl fmt::Display for Token<'_> { } } } + impl<'a> Token<'a> { const SYMBOLS: &'static [u8] = b".#[]"; fn symbol_from_byte(byte: u8) -> Option { diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 2adfb6db6c..c1e283a5f4 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -21,32 +21,47 @@ pub enum ApplyError { #[error("attempted to apply `{from_kind}` to `{to_kind}`")] /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to an enum. MismatchedKinds { + /// Kind of the value we attempted to apply. from_kind: ReflectKind, + /// Kind of the type we attempted to apply the value to. to_kind: ReflectKind, }, #[error("enum variant `{variant_name}` doesn't have a field named `{field_name}`")] /// Enum variant that we tried to apply to was missing a field. MissingEnumField { + /// Name of the enum variant. variant_name: Box, + /// Name of the missing field. field_name: Box, }, #[error("`{from_type}` is not `{to_type}`")] /// Tried to apply incompatible types. MismatchedTypes { + /// Type of the value we attempted to apply. from_type: Box, + /// Type we attempted to apply the value to. to_type: Box, }, #[error("attempted to apply type with {from_size} size to a type with {to_size} size")] - /// Attempted to apply to types with mismatched sizes, e.g. a [u8; 4] to [u8; 3]. - DifferentSize { from_size: usize, to_size: usize }, + /// Attempted to apply an [array-like] type to another of different size, e.g. a [u8; 4] to [u8; 3]. + /// + /// [array-like]: crate::Array + DifferentSize { + /// Size of the value we attempted to apply, in elements. + from_size: usize, + /// Size of the type we attempted to apply the value to, in elements. + to_size: usize, + }, #[error("variant with name `{variant_name}` does not exist on enum `{enum_name}`")] /// The enum we tried to apply to didn't contain a variant with the give name. UnknownVariant { + /// Name of the enum. enum_name: Box, + /// Name of the missing variant. variant_name: Box, }, } @@ -135,34 +150,37 @@ where /// Applies a reflected value to this value. /// - /// If a type implements an [introspection subtrait], then the semantics of this + /// If `Self` implements a [reflection subtrait], then the semantics of this /// method are as follows: - /// - If `T` is a [`Struct`], then the value of each named field of `value` is + /// - If `Self` is a [`Struct`], then the value of each named field of `value` is /// applied to the corresponding named field of `self`. Fields which are /// not present in both structs are ignored. - /// - If `T` is a [`TupleStruct`] or [`Tuple`], then the value of each + /// - If `Self` is a [`TupleStruct`] or [`Tuple`], then the value of each /// numbered field is applied to the corresponding numbered field of /// `self.` Fields which are not present in both values are ignored. - /// - If `T` is an [`Enum`], then the variant of `self` is `updated` to match + /// - If `Self` is an [`Enum`], then the variant of `self` is `updated` to match /// the variant of `value`. The corresponding fields of that variant are /// applied from `value` onto `self`. Fields which are not present in both /// values are ignored. - /// - If `T` is a [`List`] or [`Array`], then each element of `value` is applied + /// - If `Self` is a [`List`] or [`Array`], then each element of `value` is applied /// to the corresponding element of `self`. Up to `self.len()` items are applied, /// and excess elements in `value` are appended to `self`. - /// - If `T` is a [`Map`], then for each key in `value`, the associated + /// - If `Self` is a [`Map`], then for each key in `value`, the associated /// value is applied to the value associated with the same key in `self`. /// Keys which are not present in `self` are inserted. - /// - If `T` is none of these, then `value` is downcast to `T`, cloned, and + /// - If `Self` is a [`Set`], then each element of `value` is applied to the corresponding + /// element of `Self`. If an element of `value` does not exist in `Self` then it is + /// cloned and inserted. + /// - If `Self` is none of these, then `value` is downcast to `Self`, cloned, and /// assigned to `self`. /// - /// Note that `Reflect` must be implemented manually for [`List`]s and - /// [`Map`]s in order to achieve the correct semantics, as derived + /// Note that `Reflect` must be implemented manually for [`List`]s, + /// [`Map`]s, and [`Set`]s in order to achieve the correct semantics, as derived /// implementations will have the semantics for [`Struct`], [`TupleStruct`], [`Enum`] - /// or none of the above depending on the kind of type. For lists and maps, use the - /// [`list_apply`] and [`map_apply`] helper functions when implementing this method. + /// or none of the above depending on the kind of type. For lists, maps, and sets, use the + /// [`list_apply`], [`map_apply`], and [`set_apply`] helper functions when implementing this method. /// - /// [introspection subtrait]: crate#the-introspection-subtraits + /// [reflection subtrait]: crate#the-reflection-subtraits /// [`Struct`]: crate::Struct /// [`TupleStruct`]: crate::TupleStruct /// [`Tuple`]: crate::Tuple @@ -170,17 +188,19 @@ where /// [`List`]: crate::List /// [`Array`]: crate::Array /// [`Map`]: crate::Map + /// [`Set`]: crate::Set /// [`list_apply`]: crate::list_apply /// [`map_apply`]: crate::map_apply + /// [`set_apply`]: crate::set_apply /// /// # Panics /// /// Derived implementations of this method will panic: - /// - If the type of `value` is not of the same kind as `T` (e.g. if `T` is + /// - If the type of `value` is not of the same kind as `Self` (e.g. if `Self` is /// a `List`, while `value` is a `Struct`). - /// - If `T` is any complex type and the corresponding fields or elements of + /// - If `Self` is any complex type and the corresponding fields or elements of /// `self` and `value` are not of the same type. - /// - If `T` is an opaque type and `self` cannot be downcast to `T` + /// - If `Self` is an opaque type and `value` cannot be downcast to `Self` fn apply(&mut self, value: &dyn PartialReflect) { PartialReflect::try_apply(self, value).unwrap(); } @@ -570,7 +590,7 @@ impl TypePath for dyn Reflect { macro_rules! impl_full_reflect { ($(<$($id:ident),* $(,)?>)? for $ty:ty $(where $($tt:tt)*)?) => { impl $(<$($id),*>)? $crate::Reflect for $ty $(where $($tt)*)? { - fn into_any(self: Box) -> Box { + fn into_any(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { self } @@ -582,7 +602,7 @@ macro_rules! impl_full_reflect { self } - fn into_reflect(self: Box) -> Box { + fn into_reflect(self: bevy_platform::prelude::Box) -> bevy_platform::prelude::Box { self } @@ -596,8 +616,8 @@ macro_rules! impl_full_reflect { fn set( &mut self, - value: Box, - ) -> Result<(), Box> { + value: bevy_platform::prelude::Box, + ) -> Result<(), bevy_platform::prelude::Box> { *self = ::take(value)?; Ok(()) } diff --git a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs index f92a8e68e2..8a216f87b9 100644 --- a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs @@ -42,6 +42,9 @@ use serde::Deserializer; /// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [via the registry]: TypeRegistry::register_type_data pub trait DeserializeWithRegistry<'de>: Sized { + /// Deserialize this value using the given [Deserializer] and [`TypeRegistry`]. + /// + /// [`Deserializer`]: ::serde::Deserializer fn deserialize(deserializer: D, registry: &TypeRegistry) -> Result where D: Deserializer<'de>; diff --git a/crates/bevy_reflect/src/serde/de/error_utils.rs b/crates/bevy_reflect/src/serde/de/error_utils.rs index d570c47f0c..58adcfe920 100644 --- a/crates/bevy_reflect/src/serde/de/error_utils.rs +++ b/crates/bevy_reflect/src/serde/de/error_utils.rs @@ -23,7 +23,7 @@ thread_local! { pub(super) fn make_custom_error(msg: impl Display) -> E { #[cfg(feature = "debug_stack")] return TYPE_INFO_STACK - .with_borrow(|stack| E::custom(format_args!("{} (stack: {:?})", msg, stack))); + .with_borrow(|stack| E::custom(format_args!("{msg} (stack: {stack:?})"))); #[cfg(not(feature = "debug_stack"))] return E::custom(msg); } diff --git a/crates/bevy_reflect/src/serde/de/registrations.rs b/crates/bevy_reflect/src/serde/de/registrations.rs index adc0025c54..768b8ed32f 100644 --- a/crates/bevy_reflect/src/serde/de/registrations.rs +++ b/crates/bevy_reflect/src/serde/de/registrations.rs @@ -15,6 +15,7 @@ pub struct TypeRegistrationDeserializer<'a> { } impl<'a> TypeRegistrationDeserializer<'a> { + /// Creates a new [`TypeRegistrationDeserializer`]. pub fn new(registry: &'a TypeRegistry) -> Self { Self { registry } } diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index a2c3fe63ed..2ee47d4a7f 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -1,3 +1,5 @@ +//! Serde integration for reflected types. + mod de; mod ser; mod type_data; @@ -400,7 +402,7 @@ mod tests { }; // Poor man's comparison since we can't derive PartialEq for Arc - assert_eq!(format!("{:?}", expected), format!("{:?}", output)); + assert_eq!(format!("{expected:?}"), format!("{output:?}",)); let unexpected = Level { name: String::from("Level 1"), @@ -414,7 +416,7 @@ mod tests { }; // Poor man's comparison since we can't derive PartialEq for Arc - assert_ne!(format!("{:?}", unexpected), format!("{:?}", output)); + assert_ne!(format!("{unexpected:?}"), format!("{output:?}")); } #[test] diff --git a/crates/bevy_reflect/src/serde/ser/error_utils.rs b/crates/bevy_reflect/src/serde/ser/error_utils.rs index d252e7f591..8f38a0742a 100644 --- a/crates/bevy_reflect/src/serde/ser/error_utils.rs +++ b/crates/bevy_reflect/src/serde/ser/error_utils.rs @@ -23,7 +23,7 @@ thread_local! { pub(super) fn make_custom_error(msg: impl Display) -> E { #[cfg(feature = "debug_stack")] return TYPE_INFO_STACK - .with_borrow(|stack| E::custom(format_args!("{} (stack: {:?})", msg, stack))); + .with_borrow(|stack| E::custom(format_args!("{msg} (stack: {stack:?})"))); #[cfg(not(feature = "debug_stack"))] return E::custom(msg); } diff --git a/crates/bevy_reflect/src/serde/ser/processor.rs b/crates/bevy_reflect/src/serde/ser/processor.rs index cf31ab7566..fc35ff883a 100644 --- a/crates/bevy_reflect/src/serde/ser/processor.rs +++ b/crates/bevy_reflect/src/serde/ser/processor.rs @@ -112,15 +112,15 @@ use crate::{PartialReflect, TypeRegistry}; /// } /// } /// -/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result, AssetError> { -/// let mut asset_bytes = Vec::new(); +/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result { +/// let mut asset_string = String::new(); /// /// let processor = HandleProcessor; /// let serializer = ReflectSerializer::with_processor(asset, type_registry, &processor); -/// let mut ron_serializer = ron::Serializer::new(&mut asset_bytes, None)?; +/// let mut ron_serializer = ron::Serializer::new(&mut asset_string, None)?; /// /// serializer.serialize(&mut ron_serializer)?; -/// Ok(asset_bytes) +/// Ok(asset_string) /// } /// ``` /// diff --git a/crates/bevy_reflect/src/serde/ser/serializable.rs b/crates/bevy_reflect/src/serde/ser/serializable.rs index 6a8a4c978f..c83737c842 100644 --- a/crates/bevy_reflect/src/serde/ser/serializable.rs +++ b/crates/bevy_reflect/src/serde/ser/serializable.rs @@ -3,7 +3,9 @@ use core::ops::Deref; /// A type-erased serializable value. pub enum Serializable<'a> { + /// An owned serializable value. Owned(Box), + /// An immutable reference to a serializable value. Borrowed(&'a dyn erased_serde::Serialize), } diff --git a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs index 9c5bfb06f1..f9e6370799 100644 --- a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs @@ -40,6 +40,9 @@ use serde::{Serialize, Serializer}; /// [`ReflectSerializer`]: crate::serde::ReflectSerializer /// [via the registry]: TypeRegistry::register_type_data pub trait SerializeWithRegistry { + /// Serialize this value using the given [Serializer] and [`TypeRegistry`]. + /// + /// [`Serializer`]: ::serde::Serializer fn serialize(&self, serializer: S, registry: &TypeRegistry) -> Result where S: Serializer; diff --git a/crates/bevy_reflect/src/set.rs b/crates/bevy_reflect/src/set.rs index b1b9147e4e..01888e7825 100644 --- a/crates/bevy_reflect/src/set.rs +++ b/crates/bevy_reflect/src/set.rs @@ -158,8 +158,7 @@ impl DynamicSet { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Set(_)), - "expected TypeInfo::Set but received: {:?}", - represented_type + "expected TypeInfo::Set but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/std_traits.rs b/crates/bevy_reflect/src/std_traits.rs index cad001132b..9b7f46c300 100644 --- a/crates/bevy_reflect/src/std_traits.rs +++ b/crates/bevy_reflect/src/std_traits.rs @@ -1,3 +1,5 @@ +//! Module containing the [`ReflectDefault`] type. + use crate::{FromType, Reflect}; use alloc::boxed::Box; @@ -10,6 +12,7 @@ pub struct ReflectDefault { } impl ReflectDefault { + /// Returns the default value for a type. pub fn default(&self) -> Box { (self.default)() } diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index b6284a8d79..e419947b3a 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -71,6 +71,7 @@ pub trait Struct: PartialReflect { /// Returns an iterator over the values of the reflectable fields for this struct. fn iter_fields(&self) -> FieldIter; + /// Creates a new [`DynamicStruct`] from this struct. fn to_dynamic_struct(&self) -> DynamicStruct { let mut dynamic_struct = DynamicStruct::default(); dynamic_struct.set_represented_type(self.get_represented_type_info()); @@ -192,6 +193,7 @@ pub struct FieldIter<'a> { } impl<'a> FieldIter<'a> { + /// Creates a new [`FieldIter`]. pub fn new(value: &'a dyn Struct) -> Self { FieldIter { struct_val: value, @@ -292,8 +294,7 @@ impl DynamicStruct { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Struct(_)), - "expected TypeInfo::Struct but received: {:?}", - represented_type + "expected TypeInfo::Struct but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 9f81d274ae..97da69b5e2 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -76,6 +76,7 @@ pub struct TupleFieldIter<'a> { } impl<'a> TupleFieldIter<'a> { + /// Creates a new [`TupleFieldIter`]. pub fn new(value: &'a dyn Tuple) -> Self { TupleFieldIter { tuple: value, @@ -227,8 +228,7 @@ impl DynamicTuple { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::Tuple(_)), - "expected TypeInfo::Tuple but received: {:?}", - represented_type + "expected TypeInfo::Tuple but received: {represented_type:?}" ); } self.represented_type = represented_type; @@ -649,17 +649,29 @@ macro_rules! impl_reflect_tuple { } impl_reflect_tuple! {} + impl_reflect_tuple! {0: A} + impl_reflect_tuple! {0: A, 1: B} + impl_reflect_tuple! {0: A, 1: B, 2: C} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K} + impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L} macro_rules! impl_type_path_tuple { diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index 410a794f68..cceab9904e 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -146,6 +146,7 @@ pub struct TupleStructFieldIter<'a> { } impl<'a> TupleStructFieldIter<'a> { + /// Creates a new [`TupleStructFieldIter`]. pub fn new(value: &'a dyn TupleStruct) -> Self { TupleStructFieldIter { tuple_struct: value, @@ -242,8 +243,7 @@ impl DynamicTupleStruct { if let Some(represented_type) = represented_type { assert!( matches!(represented_type, TypeInfo::TupleStruct(_)), - "expected TypeInfo::TupleStruct but received: {:?}", - represented_type + "expected TypeInfo::TupleStruct but received: {represented_type:?}" ); } diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 1a3be15c36..122ace0293 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -169,7 +169,9 @@ pub enum TypeInfoError { /// [kind]: ReflectKind #[error("kind mismatch: expected {expected:?}, received {received:?}")] KindMismatch { + /// Expected kind. expected: ReflectKind, + /// Received kind. received: ReflectKind, }, } @@ -183,7 +185,7 @@ pub enum TypeInfoError { /// 3. [`PartialReflect::get_represented_type_info`] /// 4. [`TypeRegistry::get_type_info`] /// -/// Each return a static reference to [`TypeInfo`], but they all have their own use cases. +/// Each returns a static reference to [`TypeInfo`], but they all have their own use cases. /// For example, if you know the type at compile time, [`Typed::type_info`] is probably /// the simplest. If you have a `dyn Reflect` you can use [`DynamicTyped::reflect_type_info`]. /// If all you have is a `dyn PartialReflect`, you'll probably want [`PartialReflect::get_represented_type_info`]. @@ -199,14 +201,40 @@ pub enum TypeInfoError { /// [type path]: TypePath::type_path #[derive(Debug, Clone)] pub enum TypeInfo { + /// Type information for a [struct-like] type. + /// + /// [struct-like]: crate::Struct Struct(StructInfo), + /// Type information for a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: crate::TupleStruct TupleStruct(TupleStructInfo), + /// Type information for a [tuple-like] type. + /// + /// [tuple-like]: crate::Tuple Tuple(TupleInfo), + /// Type information for a [list-like] type. + /// + /// [list-like]: crate::List List(ListInfo), + /// Type information for an [array-like] type. + /// + /// [array-like]: crate::Array Array(ArrayInfo), + /// Type information for a [map-like] type. + /// + /// [map-like]: crate::Map Map(MapInfo), + /// Type information for a [set-like] type. + /// + /// [set-like]: crate::Set Set(SetInfo), + /// Type information for an [enum-like] type. + /// + /// [enum-like]: crate::Enum Enum(EnumInfo), + /// Type information for an opaque type - see the [`OpaqueInfo`] docs for + /// a discussion of opaque types. Opaque(OpaqueInfo), } @@ -557,6 +585,7 @@ pub struct OpaqueInfo { } impl OpaqueInfo { + /// Creates a new [`OpaqueInfo`]. pub fn new() -> Self { Self { ty: Type::of::(), diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 5827ebdac5..a20074b827 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -38,6 +38,7 @@ pub struct TypeRegistry { /// A synchronized wrapper around a [`TypeRegistry`]. #[derive(Clone, Default)] pub struct TypeRegistryArc { + /// The wrapped [`TypeRegistry`]. pub internal: Arc>, } @@ -313,6 +314,7 @@ impl TypeRegistry { data.insert(D::from_type()); } + /// Whether the type with given [`TypeId`] has been registered in this registry. pub fn contains(&self, type_id: TypeId) -> bool { self.registrations.contains_key(&type_id) } @@ -684,8 +686,10 @@ impl Clone for TypeRegistration { /// /// [crate-level documentation]: crate pub trait TypeData: Downcast + Send + Sync { + /// Creates a type-erased clone of this value. fn clone_type_data(&self) -> Box; } + impl_downcast!(TypeData); impl TypeData for T @@ -702,6 +706,7 @@ where /// This is used by the `#[derive(Reflect)]` macro to generate an implementation /// of [`TypeData`] to pass to [`TypeRegistration::insert`]. pub trait FromType { + /// Creates an instance of `Self` for type `T`. fn from_type() -> Self; } @@ -746,6 +751,8 @@ impl ReflectSerialize { /// [`FromType::from_type`]. #[derive(Clone)] pub struct ReflectDeserialize { + /// Function used by [`ReflectDeserialize::deserialize`] to + /// perform deserialization. pub func: fn( deserializer: &mut dyn erased_serde::Deserializer, ) -> Result, erased_serde::Error>, diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index 5735a29dbe..db8416bd6c 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -16,6 +16,7 @@ use core::{ /// /// [`Non`]: NonGenericTypeCell pub trait TypedProperty: sealed::Sealed { + /// The type of the value stored in [`GenericTypeCell`]. type Stored: 'static; } @@ -201,7 +202,7 @@ impl Default for NonGenericTypeCell { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("my_crate::foo::Foo<{}>", T::type_path())) /// } -/// +/// /// fn short_type_path() -> &'static str { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("Foo<{}>", T::short_type_path())) diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index d2e3395f77..7fc8d2b4de 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -1,37 +1,41 @@ [package] name = "bevy_remote" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "The Bevy Remote Protocol" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["http"] +default = ["http", "bevy_asset"] http = ["dep:async-io", "dep:smol-hyper"] +bevy_asset = ["dep:bevy_asset"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", features = [ "serialize", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [ + "debug", +] } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } # other anyhow = "1" hyper = { version = "1", features = ["server", "http1"] } serde = { version = "1", features = ["derive"] } -serde_json = { version = "1" } +serde_json = "1.0.140" http-body-util = "0.1" async-channel = "2" diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index b59f08604b..0bf3f9773b 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,9 +8,9 @@ use bevy_ecs::{ entity::Entity, event::EventCursor, hierarchy::ChildOf, + lifecycle::RemovedComponentEntity, query::QueryBuilder, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, - removal_detection::RemovedComponentEntity, system::{In, Local}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, }; @@ -24,7 +24,10 @@ use serde_json::{Map, Value}; use crate::{ error_codes, - schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument}, + schemas::{ + json_schema::{export_type, JsonSchemaBevyType}, + open_rpc::OpenRpcDocument, + }, BrpError, BrpResult, }; @@ -570,7 +573,8 @@ pub fn process_remote_get_watching_request( ); continue; }; - let Some(component_id) = world.components().get_id(type_registration.type_id()) else { + let Some(component_id) = world.components().get_valid_id(type_registration.type_id()) + else { let err = BrpError::component_error(format!("Unknown component: `{component_path}`")); if strict { return Err(err); @@ -718,17 +722,27 @@ pub fn process_remote_query_request(In(params): In>, world: &mut W let app_type_registry = world.resource::().clone(); let type_registry = app_type_registry.read(); - let components = get_component_ids(&type_registry, world, components, strict) + let (components, unregistered_in_components) = + get_component_ids(&type_registry, world, components, strict) + .map_err(BrpError::component_error)?; + let (option, _) = get_component_ids(&type_registry, world, option, strict) .map_err(BrpError::component_error)?; - let option = get_component_ids(&type_registry, world, option, strict) - .map_err(BrpError::component_error)?; - let has = + let (has, unregistered_in_has) = get_component_ids(&type_registry, world, has, strict).map_err(BrpError::component_error)?; - let without = get_component_ids(&type_registry, world, without, strict) + let (without, _) = get_component_ids(&type_registry, world, without, strict) .map_err(BrpError::component_error)?; - let with = get_component_ids(&type_registry, world, with, strict) + let (with, unregistered_in_with) = get_component_ids(&type_registry, world, with, strict) .map_err(BrpError::component_error)?; + // When "strict" is false: + // - Unregistered components in "option" and "without" are ignored. + // - Unregistered components in "has" are considered absent from the entity. + // - Unregistered components in "components" and "with" result in an empty + // response since they specify hard requirements. + if !unregistered_in_components.is_empty() || !unregistered_in_with.is_empty() { + return serde_json::to_value(BrpQueryResponse::default()).map_err(BrpError::internal); + } + let mut query = QueryBuilder::::new(world); for (_, component) in &components { query.ref_id(*component); @@ -784,6 +798,7 @@ pub fn process_remote_query_request(In(params): In>, world: &mut W let has_map = build_has_map( row.clone(), has_paths_and_reflect_components.iter().copied(), + &unregistered_in_has, ); response.push(BrpQueryRow { entity: row.id(), @@ -1024,12 +1039,19 @@ pub fn process_remote_remove_request( let type_registry = app_type_registry.read(); let component_ids = get_component_ids(&type_registry, world, components, true) + .and_then(|(registered, unregistered)| { + if unregistered.is_empty() { + Ok(registered) + } else { + Err(anyhow!("Unregistered component types: {:?}", unregistered)) + } + }) .map_err(BrpError::component_error)?; // Remove the components. let mut entity_world_mut = get_entity_mut(world, entity)?; - for (_, component_id) in component_ids { - entity_world_mut.remove_by_id(component_id); + for (_, component_id) in component_ids.iter() { + entity_world_mut.remove_by_id(*component_id); } Ok(Value::Null) @@ -1111,7 +1133,7 @@ pub fn process_remote_list_request(In(params): In>, world: &World) let Some(component_info) = world.components().get_info(component_id) else { continue; }; - response.push(component_info.name().to_owned()); + response.push(component_info.name().to_string()); } } // If `None`, list all registered components. @@ -1170,7 +1192,7 @@ pub fn process_remote_list_watching_request( let Some(component_info) = world.components().get_info(component_id) else { continue; }; - response.added.push(component_info.name().to_owned()); + response.added.push(component_info.name().to_string()); } } @@ -1183,7 +1205,7 @@ pub fn process_remote_list_watching_request( let Some(component_info) = world.components().get_info(*component_id) else { continue; }; - response.removed.push(component_info.name().to_owned()); + response.removed.push(component_info.name().to_string()); } } } @@ -1204,24 +1226,27 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br Some(params) => parse(params)?, }; + let extra_info = world.resource::(); let types = world.resource::(); let types = types.read(); let schemas = types .iter() - .map(crate::schemas::json_schema::export_type) - .filter(|(_, schema)| { - if let Some(crate_name) = &schema.crate_name { + .filter_map(|type_reg| { + let path_table = type_reg.type_info().type_path_table(); + if let Some(crate_name) = &path_table.crate_name() { if !filter.with_crates.is_empty() && !filter.with_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } if !filter.without_crates.is_empty() && filter.without_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } } + let (id, schema) = export_type(type_reg, extra_info); + if !filter.type_limit.with.is_empty() && !filter .type_limit @@ -1229,7 +1254,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } if !filter.type_limit.without.is_empty() && filter @@ -1238,10 +1263,9 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } - - true + Some((id.to_string(), schema)) }) .collect::>(); @@ -1264,8 +1288,9 @@ fn get_entity_mut(world: &mut World, entity: Entity) -> Result, strict: bool, -) -> AnyhowResult> { +) -> AnyhowResult<(Vec<(TypeId, ComponentId)>, Vec)> { let mut component_ids = vec![]; + let mut unregistered_components = vec![]; for component_path in component_paths { - let type_id = get_component_type_registration(type_registry, &component_path)?.type_id(); - let Some(component_id) = world.components().get_id(type_id) else { - if strict { - return Err(anyhow!( - "Component `{}` isn't used in the world", - component_path - )); - } - continue; - }; - - component_ids.push((type_id, component_id)); + let maybe_component_tuple = get_component_type_registration(type_registry, &component_path) + .ok() + .and_then(|type_registration| { + let type_id = type_registration.type_id(); + world + .components() + .get_valid_id(type_id) + .map(|component_id| (type_id, component_id)) + }); + if let Some((type_id, component_id)) = maybe_component_tuple { + component_ids.push((type_id, component_id)); + } else if strict { + return Err(anyhow!( + "Component `{}` isn't registered or used in the world", + component_path + )); + } else { + unregistered_components.push(component_path); + } } - Ok(component_ids) + Ok((component_ids, unregistered_components)) } /// Given an entity (`entity_ref`) and a list of reflected component information @@ -1325,12 +1358,16 @@ fn build_components_map<'a>( Ok(serialized_components_map) } -/// Given an entity (`entity_ref`) and list of reflected component information -/// (`paths_and_reflect_components`), return a map which associates each component to -/// a boolean value indicating whether or not that component is present on the entity. +/// Given an entity (`entity_ref`), +/// a list of reflected component information (`paths_and_reflect_components`) +/// and a list of unregistered components, +/// return a map which associates each component to a boolean value indicating +/// whether or not that component is present on the entity. +/// Unregistered components are considered absent from the entity. fn build_has_map<'a>( entity_ref: FilteredEntityRef, paths_and_reflect_components: impl Iterator, + unregistered_components: &[String], ) -> HashMap { let mut has_map = >::default(); @@ -1338,6 +1375,9 @@ fn build_has_map<'a>( let has = reflect_component.contains(entity_ref.clone()); has_map.insert(type_path.to_owned(), Value::Bool(has)); } + unregistered_components.iter().for_each(|component| { + has_map.insert(component.to_owned(), Value::Bool(false)); + }); has_map } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 97b2e453e7..348be8089d 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -364,6 +364,8 @@ //! [fully-qualified type names]: bevy_reflect::TypePath::type_path //! [fully-qualified type name]: bevy_reflect::TypePath::type_path +extern crate alloc; + use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -539,6 +541,7 @@ impl Plugin for RemotePlugin { .insert_after(Last, RemoteLast); app.insert_resource(remote_methods) + .init_resource::() .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) .configure_sets( diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 3fcc588f92..4e56625bc8 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -1,47 +1,63 @@ //! Module with JSON Schema type for Bevy Registry Types. //! It tries to follow this standard: -use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use alloc::borrow::Cow; use bevy_platform::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, VariantInfo, + GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry, + VariantInfo, }; use core::any::TypeId; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -/// Exports schema info for a given type -pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - (reg.type_info().type_path().to_owned(), reg.into()) -} +use crate::schemas::SchemaTypesMetadata; -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } +/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType` +pub trait TypeRegistrySchemaReader { + /// Export type JSON Schema. + fn export_type_json_schema( + &self, + extra_info: &SchemaTypesMetadata, + ) -> Option { + self.export_type_json_schema_for_id(extra_info, TypeId::of::()) } - result + /// Export type JSON Schema. + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option; } -impl From<&TypeRegistration> for JsonSchemaBevyType { - fn from(reg: &TypeRegistration) -> Self { +impl TypeRegistrySchemaReader for TypeRegistry { + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option { + let type_reg = self.get(type_id)?; + Some((type_reg, extra_info).into()) + } +} + +/// Exports schema info for a given type +pub fn export_type( + reg: &TypeRegistration, + metadata: &SchemaTypesMetadata, +) -> (Cow<'static, str>, JsonSchemaBevyType) { + (reg.type_info().type_path().into(), (reg, metadata).into()) +} + +impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { + fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self { + let (reg, metadata) = value; let t = reg.type_info(); let binding = t.type_path_table(); let short_path = binding.short_path(); let type_path = binding.path(); let mut typed_schema = JsonSchemaBevyType { - reflect_types: get_registered_reflect_types(reg), + reflect_types: metadata.get_registered_reflect_types(reg), short_path: short_path.to_owned(), type_path: type_path.to_owned(), crate_name: binding.crate_name().map(str::to_owned), @@ -351,8 +367,12 @@ impl SchemaJsonReference for &NamedField { #[cfg(test)] mod tests { use super::*; + use bevy_ecs::prelude::ReflectComponent; + use bevy_ecs::prelude::ReflectResource; + use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; - use bevy_reflect::Reflect; + use bevy_reflect::prelude::ReflectDefault; + use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; #[test] fn reflect_export_struct() { @@ -373,7 +393,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), @@ -418,7 +438,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -453,7 +473,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), "Should not be a component" @@ -466,6 +486,62 @@ mod tests { assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); } + #[test] + fn reflect_struct_with_custom_type_data() { + #[derive(Reflect, Default, Deserialize, Serialize)] + #[reflect(Default)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + #[derive(Clone)] + pub struct ReflectCustomData; + + impl bevy_reflect::FromType for ReflectCustomData { + fn from_type() -> Self { + ReflectCustomData + } + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + register.register_type_data::(); + } + let mut metadata = SchemaTypesMetadata::default(); + metadata.map_type_data::("CustomData"); + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration, &metadata); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a component" + ); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a resource" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have default" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have CustomData" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + #[test] fn reflect_export_tuple_struct() { #[derive(Reflect, Component, Default, Deserialize, Serialize)] @@ -482,7 +558,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -513,7 +589,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); let value = json!({ "shortPath": "Foo", @@ -538,6 +614,31 @@ mod tests { "a" ] }); - assert_eq!(schema_as_value, value); + assert_normalized_values(schema_as_value, value); + } + + /// This function exist to avoid false failures due to ordering differences between `serde_json` values. + fn assert_normalized_values(mut one: Value, mut two: Value) { + normalize_json(&mut one); + normalize_json(&mut two); + assert_eq!(one, two); + + /// Recursively sorts arrays in a `serde_json::Value` + fn normalize_json(value: &mut Value) { + match value { + Value::Array(arr) => { + for v in arr.iter_mut() { + normalize_json(v); + } + arr.sort_by_key(ToString::to_string); // Sort by stringified version + } + Value::Object(map) => { + for (_k, v) in map.iter_mut() { + normalize_json(v); + } + } + _ => {} + } + } } } diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 7104fd5547..10cb2e9421 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -1,4 +1,68 @@ //! Module with schemas used for various BRP endpoints +use bevy_ecs::{ + reflect::{ReflectComponent, ReflectResource}, + resource::Resource, +}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize, TypeData, + TypeRegistration, +}; +use core::any::TypeId; pub mod json_schema; pub mod open_rpc; + +/// Holds mapping of reflect [type data](TypeData) to strings, +/// later on used in Bevy Json Schema. +#[derive(Debug, Resource, Reflect)] +#[reflect(Resource)] +pub struct SchemaTypesMetadata { + /// Type Data id mapping to strings. + pub type_data_map: HashMap, +} + +impl Default for SchemaTypesMetadata { + fn default() -> Self { + let mut data_types = Self { + type_data_map: Default::default(), + }; + data_types.map_type_data::("Component"); + data_types.map_type_data::("Resource"); + data_types.map_type_data::("Default"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("Asset"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("AssetHandle"); + data_types.map_type_data::("Serialize"); + data_types.map_type_data::("Deserialize"); + data_types + } +} + +impl SchemaTypesMetadata { + /// Map `TypeId` of `TypeData` to string + pub fn map_type_data(&mut self, name: impl Into) { + self.type_data_map.insert(TypeId::of::(), name.into()); + } + + /// Build reflect types list for a given type registration + pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec { + self.type_data_map + .iter() + .filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone()))) + .collect() + } + + /// Checks if slice contains string value that matches checked `TypeData` + pub fn has_type_data(&self, types_string_slice: &[String]) -> bool { + self.has_type_data_by_id(TypeId::of::(), types_string_slice) + } + + /// Checks if slice contains string value that matches checked `TypeData` by id. + pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { + self.type_data_map + .get(&id) + .is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s))) + } +} diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index aa6b6e239c..143a1f0270 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_render" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides rendering functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -49,27 +49,29 @@ detailed_trace = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [ "serialize", "wgpu-types", ] } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render_macros = { path = "macros", version = "0.16.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render_macros = { path = "macros", version = "0.17.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", features = [ + "wgpu_wrapper", +] } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } @@ -117,7 +119,7 @@ wesl = { version = "0.1.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.17", default-features = false, features = [ +naga_oil = { version = "0.17.1", default-features = false, features = [ "test_shader", ] } @@ -125,7 +127,7 @@ naga_oil = { version = "0.17", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17" +naga_oil = "0.17.1" js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', @@ -138,22 +140,19 @@ web-sys = { version = "0.3.67", features = [ ] } wasm-bindgen = "0.2" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] -send_wrapper = "0.6.0" - [lints] workspace = true diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index c3fc40b23e..016fe88765 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_render_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Derive implementations for bevy_render" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_render/macros/src/extract_component.rs b/crates/bevy_render/macros/src/extract_component.rs index 2bfd0e0e11..8526f7b889 100644 --- a/crates/bevy_render/macros/src/extract_component.rs +++ b/crates/bevy_render/macros/src/extract_component.rs @@ -43,7 +43,7 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { type QueryFilter = #filter; type Out = Self; - fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_render/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 7a04932bcd..75cbdfa959 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -80,12 +80,10 @@ pub fn derive_render_label(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("render_graph").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path .segments .push(format_ident!("RenderLabel").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "RenderLabel", &trait_path, &dyn_eq_path) + derive_label(input, "RenderLabel", &trait_path) } /// Derive macro generating an impl of the trait `RenderSubGraph`. @@ -98,10 +96,8 @@ pub fn derive_render_sub_graph(input: TokenStream) -> TokenStream { trait_path .segments .push(format_ident!("render_graph").into()); - let mut dyn_eq_path = trait_path.clone(); trait_path .segments .push(format_ident!("RenderSubGraph").into()); - dyn_eq_path.segments.push(format_ident!("DynEq").into()); - derive_label(input, "RenderSubGraph", &trait_path, &dyn_eq_path) + derive_label(input, "RenderSubGraph", &trait_path) } diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 6032ff436e..ea5970431a 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -392,6 +392,10 @@ where } /// The buffer of GPU preprocessing work items for a single view. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] pub enum PreprocessWorkItemBuffers { /// The work items we use if we aren't using indirect drawing. /// diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 95218b7a59..2bddcd0d05 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -13,7 +13,7 @@ use crate::{ sync_world::{RenderEntity, SyncToRenderWorld}, texture::GpuImage, view::{ - ColorGrading, ExtractedView, ExtractedWindows, Msaa, NoIndirectDrawing, RenderLayers, + ColorGrading, ExtractedView, ExtractedWindows, Hdr, Msaa, NoIndirectDrawing, RenderLayers, RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset, Visibility, VisibleEntities, }, Extract, @@ -22,9 +22,10 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - component::{Component, HookContext}, + component::Component, entity::{ContainsEntity, Entity}, event::EventReader, + lifecycle::HookContext, prelude::With, query::Has, reflect::ReflectComponent, @@ -356,9 +357,6 @@ pub struct Camera { pub computed: ComputedCameraValues, /// The "target" that this camera will render to. pub target: RenderTarget, - /// If this is set to `true`, the camera will use an intermediate "high dynamic range" render texture. - /// This allows rendering with a wider range of lighting values. - pub hdr: bool, // todo: reflect this when #6042 lands /// The [`CameraOutputMode`] for this camera. #[reflect(ignore, clone)] @@ -389,7 +387,6 @@ impl Default for Camera { computed: Default::default(), target: Default::default(), output_mode: Default::default(), - hdr: false, msaa_writeback: true, clear_color: Default::default(), sub_camera_view: None, @@ -604,8 +601,7 @@ impl Camera { rect_relative.y = 1.0 - rect_relative.y; let ndc = rect_relative * 2. - Vec2::ONE; - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); // Using EPSILON because an ndc with Z = 0 returns NaNs. let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); @@ -671,7 +667,7 @@ impl Camera { ) -> Option { // Build a transformation matrix to convert from world space to NDC using camera data let clip_from_world: Mat4 = - self.computed.clip_from_view * camera_transform.compute_matrix().inverse(); + self.computed.clip_from_view * camera_transform.to_matrix().inverse(); let ndc_space_coords: Vec3 = clip_from_world.project_point3(world_position); (!ndc_space_coords.is_nan()).then_some(ndc_space_coords) @@ -692,8 +688,7 @@ impl Camera { /// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled. pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { // Build a transformation matrix to convert from NDC to world space using camera data - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_space_coords = ndc_to_world.project_point3(ndc); @@ -719,12 +714,13 @@ impl Camera { } } -/// Control how this camera outputs once rendering is completed. +/// Control how this [`Camera`] outputs once rendering is completed. #[derive(Debug, Clone, Copy)] pub enum CameraOutputMode { /// Writes the camera output to configured render target. Write { /// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture. + /// If not set, the output will be written as-is, ignoring `clear_color` and the existing data in the final render target texture. blend_state: Option, /// The clear color operation to perform on the final render target texture. clear_color: ClearColorConfig, @@ -1064,6 +1060,7 @@ pub fn camera_system( #[reflect(opaque)] #[reflect(Component, Default, Clone)] pub struct CameraMainTextureUsages(pub TextureUsages); + impl Default for CameraMainTextureUsages { fn default() -> Self { Self( @@ -1074,6 +1071,13 @@ impl Default for CameraMainTextureUsages { } } +impl CameraMainTextureUsages { + pub fn with(mut self, usages: TextureUsages) -> Self { + self.0 |= usages; + self + } +} + #[derive(Component, Debug)] pub struct ExtractedCamera { pub target: Option, @@ -1101,9 +1105,11 @@ pub fn extract_cameras( &GlobalTransform, &VisibleEntities, &Frustum, + Has, Option<&ColorGrading>, Option<&Exposure>, Option<&TemporalJitter>, + Option<&MipBias>, Option<&RenderLayers>, Option<&Projection>, Has, @@ -1122,9 +1128,11 @@ pub fn extract_cameras( transform, visible_entities, frustum, + hdr, color_grading, exposure, temporal_jitter, + mip_bias, render_layers, projection, no_indirect_drawing, @@ -1136,6 +1144,7 @@ pub fn extract_cameras( ExtractedView, RenderVisibleEntities, TemporalJitter, + MipBias, RenderLayers, Projection, NoIndirectDrawing, @@ -1200,14 +1209,14 @@ pub fn extract_cameras( exposure: exposure .map(Exposure::exposure) .unwrap_or_else(|| Exposure::default().exposure()), - hdr: camera.hdr, + hdr, }, ExtractedView { retained_view_entity: RetainedViewEntity::new(main_entity.into(), None, 0), clip_from_view: camera.clip_from_view(), world_from_view: *transform, clip_from_world: None, - hdr: camera.hdr, + hdr, viewport: UVec4::new( viewport_origin.x, viewport_origin.y, @@ -1222,14 +1231,26 @@ pub fn extract_cameras( if let Some(temporal_jitter) = temporal_jitter { commands.insert(temporal_jitter.clone()); + } else { + commands.remove::(); + } + + if let Some(mip_bias) = mip_bias { + commands.insert(mip_bias.clone()); + } else { + commands.remove::(); } if let Some(render_layers) = render_layers { commands.insert(render_layers.clone()); + } else { + commands.remove::(); } if let Some(perspective) = projection { commands.insert(perspective.clone()); + } else { + commands.remove::(); } if no_indirect_drawing @@ -1239,6 +1260,8 @@ pub fn extract_cameras( ) { commands.insert(NoIndirectDrawing); + } else { + commands.remove::(); } }; } @@ -1339,6 +1362,12 @@ impl TemporalJitter { /// Camera component specifying a mip bias to apply when sampling from material textures. /// /// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness. -#[derive(Default, Component, Reflect)] +#[derive(Component, Reflect, Clone)] #[reflect(Default, Component)] pub struct MipBias(pub f32); + +impl Default for MipBias { + fn default() -> Self { + Self(-1.0) + } +} diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index 157bcf8998..6183a1d4de 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -6,7 +6,9 @@ use bevy_reflect::prelude::*; use derive_more::derive::From; use serde::{Deserialize, Serialize}; -/// For a camera, specifies the color used to clear the viewport before rendering. +/// For a camera, specifies the color used to clear the viewport +/// [before rendering](crate::camera::Camera::clear_color) +/// or when [writing to the final render target texture](crate::camera::Camera::output_mode). #[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default, From)] #[reflect(Serialize, Deserialize, Default, Clone)] pub enum ClearColorConfig { @@ -21,10 +23,15 @@ pub enum ClearColorConfig { None, } -/// A [`Resource`] that stores the color that is used to clear the screen between frames. +/// A [`Resource`] that stores the default color that cameras use to clear the screen between frames. /// /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. +/// +/// Individual cameras may use [`Camera.clear_color`] to specify a different +/// clear color or opt out of clearing their viewport. +/// +/// [`Camera.clear_color`]: crate::camera::Camera::clear_color #[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] #[reflect(Resource, Default, Debug, Clone)] pub struct ClearColor(pub Color); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index a7796a1d1a..ee2a5080d2 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -93,8 +93,7 @@ pub trait CameraProjection { /// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system /// for each camera to update its frustum. fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum { - let clip_from_world = - self.get_clip_from_view() * camera_transform.compute_matrix().inverse(); + let clip_from_world = self.get_clip_from_view() * camera_transform.to_matrix().inverse(); Frustum::from_clip_from_world_custom_far( &clip_from_world, &camera_transform.translation(), diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index ec226c760b..e7005f70f3 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -15,7 +15,8 @@ use wgpu::{ PipelineStatisticsTypes, QuerySet, QuerySetDescriptor, QueryType, RenderPass, }; -use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue, WgpuWrapper}; +use crate::renderer::{RenderAdapterInfo, RenderDevice, RenderQueue}; +use bevy_utils::WgpuWrapper; use super::RecordDiagnostics; diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs index 7f046036a9..197b9f4e7f 100644 --- a/crates/bevy_render/src/diagnostic/mod.rs +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -148,7 +148,7 @@ pub struct PassSpanGuard<'a, R: ?Sized, P> { } impl PassSpanGuard<'_, R, P> { - /// End the span. You have to provide the same encoder which was used to begin the span. + /// End the span. You have to provide the same pass which was used to begin the span. pub fn end(self, pass: &mut P) { self.recorder.end_pass_span(pass); core::mem::forget(self); diff --git a/crates/bevy_render/src/experimental/occlusion_culling/mod.rs b/crates/bevy_render/src/experimental/occlusion_culling/mod.rs index a3b067e19f..77fcb4b5b2 100644 --- a/crates/bevy_render/src/experimental/occlusion_culling/mod.rs +++ b/crates/bevy_render/src/experimental/occlusion_culling/mod.rs @@ -4,19 +4,13 @@ //! Bevy. use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use crate::{ - extract_component::ExtractComponent, - render_resource::{Shader, TextureView}, + extract_component::ExtractComponent, load_shader_library, render_resource::TextureView, }; -/// The handle to the `mesh_preprocess_types.wgsl` compute shader. -pub const MESH_PREPROCESS_TYPES_SHADER_HANDLE: Handle = - weak_handle!("7bf7bdb1-ec53-4417-987f-9ec36533287c"); - /// Enables GPU occlusion culling. /// /// See [`OcclusionCulling`] for a detailed description of occlusion culling in @@ -25,12 +19,7 @@ pub struct OcclusionCullingPlugin; impl Plugin for OcclusionCullingPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - MESH_PREPROCESS_TYPES_SHADER_HANDLE, - "mesh_preprocess_types.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "mesh_preprocess_types.wgsl"); } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index e1f528d6ab..b7bb05e425 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -60,7 +60,7 @@ pub trait ExtractComponent: Component { // type Out: Component = Self; /// Defines how the component is transferred into the "render world". - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin prepares the components of the corresponding type for the GPU diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index a8e5a9ecbd..cd194b7c40 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -34,7 +34,7 @@ pub trait ExtractInstance: Send + Sync + Sized + 'static { type QueryFilter: QueryFilter; /// Defines how the component is transferred into the "render world". - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin extracts one or more components into the "render world" as diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index f543098474..e97c758260 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -1,7 +1,8 @@ use crate::MainWorld; use bevy_ecs::{ - component::Tick, + component::{ComponentId, Tick}, prelude::*, + query::FilteredAccessSet, system::{ ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError, SystemState, @@ -71,17 +72,31 @@ where type State = ExtractState

; type Item<'w, 's> = Extract<'w, 's, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { let mut main_world = world.resource_mut::(); ExtractState { state: SystemState::new(&mut main_world), - main_world_state: Res::::init_state(world, system_meta), + main_world_state: Res::::init_state(world), } } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Res::::init_access( + &state.main_world_state, + system_meta, + component_access_set, + world, + ); + } + #[inline] unsafe fn validate_param( - state: &Self::State, + state: &mut Self::State, _system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { @@ -97,7 +112,7 @@ where // SAFETY: We provide the main world on which this system state was initialized on. unsafe { SystemState::

::validate_param( - &state.state, + &mut state.state, main_world.as_unsafe_world_cell_readonly(), ) } diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index 49755f4098..04e4109f09 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -1,25 +1,21 @@ use crate::{ extract_resource::ExtractResource, - prelude::Shader, + load_shader_library, render_resource::{ShaderType, UniformBuffer}, renderer::{RenderDevice, RenderQueue}, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_diagnostic::FrameCount; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; use bevy_time::Time; -pub const GLOBALS_TYPE_HANDLE: Handle = - weak_handle!("9e22a765-30ca-4070-9a4c-34ac08f1c0e7"); - pub struct GlobalsPlugin; impl Plugin for GlobalsPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, GLOBALS_TYPE_HANDLE, "globals.wgsl", Shader::from_wgsl); + load_shader_library!(app, "globals.wgsl"); app.register_type::(); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index c05861f3da..adcb883559 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -15,14 +15,14 @@ use async_channel::{Receiver, Sender}; use bevy_app::{App, Plugin}; use bevy_asset::Handle; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_ecs::{ change_detection::ResMut, entity::Entity, - event::Event, + event::EntityEvent, prelude::{Component, Resource, World}, system::{Query, Res}, }; +use bevy_ecs::{event::Event, schedule::IntoScheduleConfigs}; use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; @@ -96,7 +96,7 @@ impl Readback { /// /// The event contains the data as a `Vec`, which can be interpreted as the raw bytes of the /// requested buffer or texture. -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ReadbackComplete(pub Vec); diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index b4c9011c78..a20315c099 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -9,8 +9,8 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #[cfg(target_pointer_width = "16")] @@ -72,7 +72,14 @@ pub mod prelude { }; } use batching::gpu_preprocessing::BatchingPlugin; + +#[doc(hidden)] +pub mod _macro { + pub use bevy_asset; +} + use bevy_ecs::schedule::ScheduleBuildSettings; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats}; use bevy_utils::prelude::default; pub use extract_param::Extract; @@ -86,7 +93,8 @@ use render_asset::{ use renderer::{RenderAdapter, RenderDevice, RenderQueue}; use settings::RenderResources; use sync_world::{ - despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, SyncWorldPlugin, + despawn_temporary_render_entities, entity_sync_system, MainEntity, RenderEntity, + SyncToRenderWorld, SyncWorldPlugin, TemporaryRenderEntity, }; use crate::gpu_readback::GpuReadbackPlugin; @@ -95,20 +103,39 @@ use crate::{ mesh::{MeshPlugin, MorphPlugin, RenderMesh}, render_asset::prepare_assets, render_resource::{PipelineCache, Shader, ShaderLoader}, - renderer::{render_system, RenderInstance, WgpuWrapper}, + renderer::{render_system, RenderInstance}, settings::RenderCreation, storage::StoragePlugin, view::{ViewPlugin, WindowRenderPlugin}, }; use alloc::sync::Arc; use bevy_app::{App, AppLabel, Plugin, SubApp}; -use bevy_asset::{load_internal_asset, weak_handle, AssetApp, AssetServer, Handle}; +use bevy_asset::{AssetApp, AssetServer}; use bevy_ecs::{prelude::*, schedule::ScheduleLabel}; +use bevy_utils::WgpuWrapper; use bitflags::bitflags; use core::ops::{Deref, DerefMut}; use std::sync::Mutex; use tracing::debug; +/// Inline shader as an `embedded_asset` and load it permanently. +/// +/// This works around a limitation of the shader loader not properly loading +/// dependencies of shaders. +#[macro_export] +macro_rules! load_shader_library { + ($asset_server_provider: expr, $path: literal $(, $settings: expr)?) => { + $crate::_macro::bevy_asset::embedded_asset!($asset_server_provider, $path); + let handle: $crate::_macro::bevy_asset::prelude::Handle<$crate::prelude::Shader> = + $crate::_macro::bevy_asset::load_embedded_asset!( + $asset_server_provider, + $path + $(,$settings)? + ); + core::mem::forget(handle); + } +} + /// Contains the default Bevy rendering backend based on wgpu. /// /// Rendering is done in a [`SubApp`], which exchanges data with the main app @@ -289,13 +316,6 @@ struct FutureRenderResources(Arc>>); #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] pub struct RenderApp; -pub const MATHS_SHADER_HANDLE: Handle = - weak_handle!("d94d70d4-746d-49c4-bfc3-27d63f2acda0"); -pub const COLOR_OPERATIONS_SHADER_HANDLE: Handle = - weak_handle!("33a80b2f-aaf7-4c86-b828-e7ae83b72f1a"); -pub const BINDLESS_SHADER_HANDLE: Handle = - weak_handle!("13f1baaa-41bf-448e-929e-258f9307a522"); - impl Plugin for RenderPlugin { /// Initializes the renderer, sets up the [`RenderSystems`] and creates the rendering sub-app. fn build(&self, app: &mut App) { @@ -432,6 +452,9 @@ impl Plugin for RenderPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::(); } @@ -443,29 +466,24 @@ impl Plugin for RenderPlugin { } fn finish(&self, app: &mut App) { - load_internal_asset!(app, MATHS_SHADER_HANDLE, "maths.wgsl", Shader::from_wgsl); - load_internal_asset!( - app, - COLOR_OPERATIONS_SHADER_HANDLE, - "color_operations.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - BINDLESS_SHADER_HANDLE, - "bindless.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "maths.wgsl"); + load_shader_library!(app, "color_operations.wgsl"); + load_shader_library!(app, "bindless.wgsl"); if let Some(future_render_resources) = app.world_mut().remove_resource::() { let RenderResources(device, queue, adapter_info, render_adapter, instance) = future_render_resources.0.lock().unwrap().take().unwrap(); + let compressed_image_format_support = CompressedImageFormatSupport( + CompressedImageFormats::from_features(device.features()), + ); + app.insert_resource(device.clone()) .insert_resource(queue.clone()) .insert_resource(adapter_info.clone()) - .insert_resource(render_adapter.clone()); + .insert_resource(render_adapter.clone()) + .insert_resource(compressed_image_format_support); let render_app = app.sub_app_mut(RenderApp); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index 113369cea1..c171cf3957 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -78,6 +78,9 @@ pub struct MeshAllocator { /// WebGL 2. On this platform, we must give each vertex array its own /// buffer, because we can't adjust the first vertex when we perform a draw. general_vertex_slabs_supported: bool, + + /// Additional buffer usages to add to any vertex or index buffers created. + pub extra_buffer_usages: BufferUsages, } /// Tunable parameters that customize the behavior of the allocator. @@ -158,6 +161,10 @@ pub struct MeshBufferSlice<'a> { pub struct SlabId(pub NonMaxU32); /// Data for a single slab. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] enum Slab { /// A slab that can contain multiple objects. General(GeneralSlab), @@ -344,6 +351,7 @@ impl FromWorld for MeshAllocator { mesh_id_to_index_slab: HashMap::default(), next_slab_id: default(), general_vertex_slabs_supported, + extra_buffer_usages: BufferUsages::empty(), } } } @@ -594,7 +602,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: len as u64, - usage: buffer_usages | BufferUsages::COPY_DST, + usage: buffer_usages | BufferUsages::COPY_DST | self.extra_buffer_usages, mapped_at_creation: true, }); { @@ -831,7 +839,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(), - usage: buffer_usages, + usage: buffer_usages | self.extra_buffer_usages, mapped_at_creation: false, }); diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 0a634c2598..4355892487 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -366,7 +366,7 @@ pub trait ViewNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError>; } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index a12d336018..42a6725c8c 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -213,8 +213,8 @@ pub trait RenderCommand { /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, - entity: Option>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, + entity: Option>, param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult; @@ -246,8 +246,8 @@ macro_rules! render_command_tuple_impl { )] fn render<'w>( _item: &P, - ($($view,)*): ROQueryItem<'w, Self::ViewQuery>, - maybe_entities: Option>, + ($($view,)*): ROQueryItem<'w, '_, Self::ViewQuery>, + maybe_entities: Option>, ($($name,)*): SystemParamItem<'w, '_, Self::Param>, _pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -315,7 +315,6 @@ where /// Prepares the render command to be used. This is called once and only once before the phase /// begins. There may be zero or more [`draw`](RenderCommandState::draw) calls following a call to this function. fn prepare(&mut self, world: &'_ World) { - self.state.update_archetypes(world); self.view.update_archetypes(world); self.entity.update_archetypes(world); } @@ -328,7 +327,7 @@ where view: Entity, item: &P, ) -> Result<(), DrawError> { - let param = self.state.get_manual(world); + let param = self.state.get(world); let view = match self.view.get_manual(world, view) { Ok(view) => view, Err(err) => match err { diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 2c8e984bfd..cff88bd355 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -1,4 +1,3 @@ -use crate::renderer::WgpuWrapper; use crate::{ define_atomic_id, render_asset::RenderAssets, @@ -9,6 +8,7 @@ use crate::{ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{SystemParam, SystemParamItem}; pub use bevy_render_macros::AsBindGroup; +use bevy_utils::WgpuWrapper; use core::ops::Deref; use encase::ShaderType; use thiserror::Error; diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 3aaf46183f..cc8eb188de 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -147,6 +147,13 @@ impl<'a> IntoBinding<'a> for &'a TextureView { } } +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler { } } +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + impl<'a> IntoBinding<'a> for BindingResource<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { } } +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + pub trait IntoBindingArray<'b, const N: usize> { fn into_array(self) -> [BindingResource<'b>; N]; } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout.rs b/crates/bevy_render/src/render_resource/bind_group_layout.rs index e19f5b969f..2d674f46d1 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout.rs @@ -1,5 +1,5 @@ use crate::define_atomic_id; -use crate::renderer::WgpuWrapper; +use bevy_utils::WgpuWrapper; use core::ops::Deref; define_atomic_id!(BindGroupLayoutId); diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index bc4a7d306d..41affa4349 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -568,4 +568,8 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure.into_bind_group_layout_entry_builder() + } } diff --git a/crates/bevy_render/src/render_resource/buffer.rs b/crates/bevy_render/src/render_resource/buffer.rs index 9b7bb2c41f..b779417bf5 100644 --- a/crates/bevy_render/src/render_resource/buffer.rs +++ b/crates/bevy_render/src/render_resource/buffer.rs @@ -1,5 +1,5 @@ use crate::define_atomic_id; -use crate::renderer::WgpuWrapper; +use bevy_utils::WgpuWrapper; use core::ops::{Bound, Deref, RangeBounds}; define_atomic_id!(BufferId); diff --git a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs index 195920ee0c..0c5bf36bf6 100644 --- a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs +++ b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs @@ -14,6 +14,7 @@ use wgpu::{BindingResource, BufferUsages}; /// Trait for types able to go in a [`GpuArrayBuffer`]. pub trait GpuArrayBufferable: ShaderType + ShaderSize + WriteInto + Clone {} + impl GpuArrayBufferable for T {} /// Stores an array of elements to be transferred to the GPU and made accessible to shaders as a read-only array. diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index b777d96290..aecf27173d 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -38,18 +38,21 @@ pub use wgpu::{ BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder, }, - AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, - FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, - PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, - PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, @@ -57,8 +60,9 @@ pub use wgpu::{ TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, - TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, - VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, + TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, + VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, + VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/render_resource/pipeline.rs b/crates/bevy_render/src/render_resource/pipeline.rs index 30f9a974b8..b76174cac3 100644 --- a/crates/bevy_render/src/render_resource/pipeline.rs +++ b/crates/bevy_render/src/render_resource/pipeline.rs @@ -1,12 +1,12 @@ use super::ShaderDefVal; use crate::mesh::VertexBufferLayout; -use crate::renderer::WgpuWrapper; use crate::{ define_atomic_id, render_resource::{BindGroupLayout, Shader}, }; use alloc::borrow::Cow; use bevy_asset::Handle; +use bevy_utils::WgpuWrapper; use core::ops::Deref; use wgpu::{ ColorTargetState, DepthStencilState, MultisampleState, PrimitiveState, PushConstantRange, diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index be22c83f98..9d658cf361 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -1,4 +1,3 @@ -use crate::renderer::WgpuWrapper; use crate::{ render_resource::*, renderer::{RenderAdapter, RenderDevice}, @@ -14,6 +13,7 @@ use bevy_ecs::{ use bevy_platform::collections::{hash_map::EntryRef, HashMap, HashSet}; use bevy_tasks::Task; use bevy_utils::default; +use bevy_utils::WgpuWrapper; use core::{future::Future, hash::Hash, mem, ops::Deref}; use naga::valid::Capabilities; use std::sync::{Mutex, PoisonError}; @@ -80,6 +80,10 @@ pub struct CachedPipeline { } /// State of a cached pipeline inserted into a [`PipelineCache`]. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] #[derive(Debug)] pub enum CachedPipelineState { /// The pipeline GPU object is queued for creation. @@ -135,7 +139,7 @@ struct ShaderCache { composer: naga_oil::compose::Composer, } -#[derive(Clone, PartialEq, Eq, Debug, Hash)] +#[derive(serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq, Debug, Hash)] pub enum ShaderDefVal { Bool(String, bool), Int(String, i32), @@ -189,33 +193,42 @@ impl ShaderCache { } } + #[expect( + clippy::result_large_err, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + )] fn add_import_to_composer( composer: &mut naga_oil::compose::Composer, import_path_shaders: &HashMap>, shaders: &HashMap, Shader>, import: &ShaderImport, ) -> Result<(), PipelineCacheError> { - if !composer.contains_module(&import.module_name()) { - if let Some(shader_handle) = import_path_shaders.get(import) { - if let Some(shader) = shaders.get(shader_handle) { - for import in &shader.imports { - Self::add_import_to_composer( - composer, - import_path_shaders, - shaders, - import, - )?; - } - - composer.add_composable_module(shader.into())?; - } - } - // if we fail to add a module the composer will tell us what is missing + // Early out if we've already imported this module + if composer.contains_module(&import.module_name()) { + return Ok(()); } + // Check if the import is available (this handles the recursive import case) + let shader = import_path_shaders + .get(import) + .and_then(|handle| shaders.get(handle)) + .ok_or(PipelineCacheError::ShaderImportNotYetAvailable)?; + + // Recurse down to ensure all import dependencies are met + for import in &shader.imports { + Self::add_import_to_composer(composer, import_path_shaders, shaders, import)?; + } + + composer.add_composable_module(shader.into())?; + // if we fail to add a module the composer will tell us what is missing + Ok(()) } + #[expect( + clippy::result_large_err, + reason = "See https://github.com/bevyengine/bevy/issues/19220" + )] fn get( &mut self, render_device: &RenderDevice, @@ -1101,6 +1114,10 @@ fn create_pipeline_task( } /// Type of error returned by a [`PipelineCache`] when the creation of a GPU pipeline object failed. +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] #[derive(Error, Debug)] pub enum PipelineCacheError { #[error( @@ -1176,6 +1193,10 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti Capabilities::MULTISAMPLED_SHADING, downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING), ); + capabilities.set( + Capabilities::RAY_QUERY, + features.contains(Features::EXPERIMENTAL_RAY_QUERY), + ); capabilities.set( Capabilities::DUAL_SOURCE_BLENDING, features.contains(Features::DUAL_SOURCE_BLENDING), diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 005fb07c05..ff8430b951 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -324,14 +324,21 @@ pub enum ShaderLoaderError { Parse(#[from] alloc::string::FromUtf8Error), } +/// Settings for loading shaders. +#[derive(serde::Serialize, serde::Deserialize, Debug, Default)] +pub struct ShaderSettings { + /// The `#define` specified for this shader. + pub shader_defs: Vec, +} + impl AssetLoader for ShaderLoader { type Asset = Shader; - type Settings = (); + type Settings = ShaderSettings; type Error = ShaderLoaderError; async fn load( &self, reader: &mut dyn Reader, - _settings: &Self::Settings, + settings: &Self::Settings, load_context: &mut LoadContext<'_>, ) -> Result { let ext = load_context.path().extension().unwrap().to_str().unwrap(); @@ -341,9 +348,19 @@ impl AssetLoader for ShaderLoader { let path = path.replace(std::path::MAIN_SEPARATOR, "/"); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; + if ext != "wgsl" && !settings.shader_defs.is_empty() { + tracing::warn!( + "Tried to load a non-wgsl shader with shader defs, this isn't supported: \ + The shader defs will be ignored." + ); + } let mut shader = match ext { "spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()), - "wgsl" => Shader::from_wgsl(String::from_utf8(bytes)?, path), + "wgsl" => Shader::from_wgsl_with_defs( + String::from_utf8(bytes)?, + path, + settings.shader_defs.clone(), + ), "vert" => Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Vertex, path), "frag" => { Shader::from_glsl(String::from_utf8(bytes)?, naga::ShaderStage::Fragment, path) diff --git a/crates/bevy_render/src/render_resource/texture.rs b/crates/bevy_render/src/render_resource/texture.rs index f975fc18f3..c96da8a1be 100644 --- a/crates/bevy_render/src/render_resource/texture.rs +++ b/crates/bevy_render/src/render_resource/texture.rs @@ -1,7 +1,7 @@ use crate::define_atomic_id; -use crate::renderer::WgpuWrapper; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::resource::Resource; +use bevy_utils::WgpuWrapper; use core::ops::Deref; define_atomic_id!(TextureId); diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 1691911c2c..bec30200a8 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -4,6 +4,7 @@ mod render_device; use bevy_derive::{Deref, DerefMut}; #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] use bevy_tasks::ComputeTaskPool; +use bevy_utils::WgpuWrapper; pub use graph_runner::*; pub use render_device::*; use tracing::{error, info, info_span, warn}; @@ -120,46 +121,6 @@ pub fn render_system(world: &mut World, state: &mut SystemState(T); -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -#[derive(Debug, Clone, Deref, DerefMut)] -pub struct WgpuWrapper(send_wrapper::SendWrapper); - -// SAFETY: SendWrapper is always Send + Sync. -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -unsafe impl Send for WgpuWrapper {} -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -unsafe impl Sync for WgpuWrapper {} - -#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] -impl WgpuWrapper { - pub fn new(t: T) -> Self { - Self(t) - } - - pub fn into_inner(self) -> T { - self.0 - } -} - -#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] -impl WgpuWrapper { - pub fn new(t: T) -> Self { - Self(send_wrapper::SendWrapper::new(t)) - } - - pub fn into_inner(self) -> T { - self.0.take() - } -} - /// This queue is used to enqueue tasks for the GPU to execute asynchronously. #[derive(Resource, Clone, Deref, DerefMut)] pub struct RenderQueue(pub Arc>); @@ -202,7 +163,7 @@ pub async fn initialize_renderer( if adapter_info.device_type == DeviceType::Cpu { warn!( "The selected adapter is using a driver that only supports software rendering. \ - This is likely to be very slow. See https://bevyengine.org/learn/errors/b0006/" + This is likely to be very slow. See https://bevy.org/learn/errors/b0006/" ); } @@ -219,12 +180,6 @@ pub async fn initialize_renderer( features -= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS; } - // RAY_QUERY and RAY_TRACING_ACCELERATION STRUCTURE will sometimes cause DeviceLost failures on platforms - // that report them as supported: - // - features -= wgpu::Features::EXPERIMENTAL_RAY_QUERY; - features -= wgpu::Features::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE; - limits = adapter.limits(); } diff --git a/crates/bevy_render/src/renderer/render_device.rs b/crates/bevy_render/src/renderer/render_device.rs index d33139745b..31f47e5740 100644 --- a/crates/bevy_render/src/renderer/render_device.rs +++ b/crates/bevy_render/src/renderer/render_device.rs @@ -3,8 +3,8 @@ use crate::render_resource::{ BindGroup, BindGroupLayout, Buffer, ComputePipeline, RawRenderPipelineDescriptor, RenderPipeline, Sampler, Texture, }; -use crate::WgpuWrapper; use bevy_ecs::resource::Resource; +use bevy_utils::WgpuWrapper; use wgpu::{ util::DeviceExt, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BufferAsyncError, BufferBindingType, MaintainResult, diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index d4eb9b7680..ab50fc81b1 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -152,6 +152,10 @@ pub struct RenderResources( ); /// An enum describing how the renderer will initialize resources. This is used when creating the [`RenderPlugin`](crate::RenderPlugin). +#[expect( + clippy::large_enum_variant, + reason = "See https://github.com/bevyengine/bevy/issues/19220" +)] pub enum RenderCreation { /// Allows renderer resource initialization to happen outside of the rendering plugin. Manual(RenderResources), diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index f15f3c4003..b216b5fd32 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,15 +1,16 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; +use bevy_ecs::lifecycle::{Add, Remove}; use bevy_ecs::{ component::Component, entity::{ContainsEntity, Entity, EntityEquivalent}, - observer::Trigger, + observer::On, query::With, reflect::ReflectComponent, resource::Resource, system::{Local, Query, ResMut, SystemState}, - world::{Mut, OnAdd, OnRemove, World}, + world::{Mut, World}, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -93,12 +94,12 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |trigger: On, mut pending: ResMut| { pending.push(EntityRecord::Added(trigger.target())); }, ); app.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { if let Ok(e) = query.get(trigger.target()) { @@ -126,8 +127,9 @@ pub struct SyncToRenderWorld; /// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity. /// /// Can also be used as a newtype wrapper for render world entities. -#[derive(Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Component)] +#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, Reflect)] #[component(clone_behavior = Ignore)] +#[reflect(Component, Clone)] pub struct RenderEntity(Entity); impl RenderEntity { #[inline] @@ -154,7 +156,8 @@ unsafe impl EntityEquivalent for RenderEntity {} /// Component added on the render world entities to keep track of the corresponding main world entity. /// /// Can also be used as a newtype wrapper for main world entities. -#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +#[derive(Component, Deref, Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Reflect)] +#[reflect(Component, Clone)] pub struct MainEntity(Entity); impl MainEntity { #[inline] @@ -217,10 +220,10 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl EntityRecord::Added(e) => { if let Ok(mut main_entity) = world.get_entity_mut(e) { match main_entity.entry::() { - bevy_ecs::world::Entry::Occupied(_) => { + bevy_ecs::world::ComponentEntry::Occupied(_) => { panic!("Attempting to synchronize an entity that has already been synchronized!"); } - bevy_ecs::world::Entry::Vacant(entry) => { + bevy_ecs::world::ComponentEntry::Vacant(entry) => { let id = render_world.spawn(MainEntity(e)).id(); entry.insert(RenderEntity(id)); @@ -278,7 +281,7 @@ mod render_entities_world_query_impls { archetype::Archetype, component::{ComponentId, Components, Tick}, entity::Entity, - query::{FilteredAccess, QueryData, ReadOnlyQueryData, WorldQuery}, + query::{FilteredAccess, QueryData, ReadOnlyQueryData, ReleaseStateQueryData, WorldQuery}, storage::{Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -296,9 +299,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -311,9 +314,9 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, archetype: &'w Archetype, table: &'w Table, ) { @@ -324,9 +327,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. @@ -361,21 +364,24 @@ mod render_entities_world_query_impls { unsafe impl QueryData for RenderEntity { const IS_READ_ONLY: bool = true; type ReadOnly = RenderEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. let component = - unsafe { <&RenderEntity as QueryData>::fetch(fetch, entity, table_row) }; + unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } @@ -383,6 +389,12 @@ mod render_entities_world_query_impls { // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for RenderEntity {} + impl ReleaseStateQueryData for RenderEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } + /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for MainEntity { @@ -396,9 +408,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -411,7 +423,7 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, component_id: &ComponentId, archetype: &'w Archetype, @@ -424,9 +436,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. @@ -461,26 +473,36 @@ mod render_entities_world_query_impls { unsafe impl QueryData for MainEntity { const IS_READ_ONLY: bool = true; type ReadOnly = MainEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. - let component = unsafe { <&MainEntity as QueryData>::fetch(fetch, entity, table_row) }; + let component = + unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for MainEntity {} + + impl ReleaseStateQueryData for MainEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } } #[cfg(test)] @@ -488,10 +510,11 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - observer::Trigger, + lifecycle::{Add, Remove}, + observer::On, query::With, system::{Query, ResMut}, - world::{OnAdd, OnRemove, World}, + world::World, }; use super::{ @@ -509,12 +532,12 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |trigger: On, mut pending: ResMut| { pending.push(EntityRecord::Added(trigger.target())); }, ); main_world.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { if let Ok(e) = query.get(trigger.target()) { diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index de5361aad8..6c2dc67aae 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -8,7 +8,10 @@ pub use crate::render_resource::DefaultImageSampler; use bevy_image::CompressedImageSaver; #[cfg(feature = "hdr")] use bevy_image::HdrTextureLoader; -use bevy_image::{CompressedImageFormats, Image, ImageLoader, ImageSamplerDescriptor}; +use bevy_image::{ + CompressedImageFormatSupport, CompressedImageFormats, Image, ImageLoader, + ImageSamplerDescriptor, +}; pub use fallback_image::*; pub use gpu_image::*; pub use texture_attachment::*; @@ -20,6 +23,7 @@ use crate::{ use bevy_app::{App, Plugin}; use bevy_asset::{weak_handle, AssetApp, Assets, Handle}; use bevy_ecs::prelude::*; +use tracing::warn; /// A handle to a 1 x 1 transparent white image. /// @@ -111,12 +115,16 @@ impl Plugin for ImagePlugin { fn finish(&self, app: &mut App) { if !ImageLoader::SUPPORTED_FORMATS.is_empty() { - let supported_compressed_formats = match app.world().get_resource::() { - Some(render_device) => { - CompressedImageFormats::from_features(render_device.features()) - } - None => CompressedImageFormats::NONE, + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); + CompressedImageFormats::NONE }; + app.register_asset_loader(ImageLoader::new(supported_compressed_formats)); } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 2f80e5f94b..b2b90d0b24 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -1,7 +1,6 @@ pub mod visibility; pub mod window; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_diagnostic::FrameCount; pub use visibility::*; pub use window::*; @@ -13,7 +12,7 @@ use crate::{ }, experimental::occlusion_culling::OcclusionCulling, extract_component::ExtractComponentPlugin, - prelude::Shader, + load_shader_library, primitives::Frustum, render_asset::RenderAssets, render_phase::ViewRangefinder3d, @@ -46,8 +45,6 @@ use wgpu::{ TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }; -pub const VIEW_TYPE_HANDLE: Handle = weak_handle!("7234423c-38bb-411c-acec-f67730f6db5b"); - /// The matrix that converts from the RGB to the LMS color space. /// /// To derive this, first we convert from RGB to [CIE 1931 XYZ]: @@ -101,7 +98,7 @@ pub struct ViewPlugin; impl Plugin for ViewPlugin { fn build(&self, app: &mut App) { - load_internal_asset!(app, VIEW_TYPE_HANDLE, "view.wgsl", Shader::from_wgsl); + load_shader_library!(app, "view.wgsl"); app.register_type::() .register_type::() @@ -114,6 +111,7 @@ impl Plugin for ViewPlugin { .register_type::() // NOTE: windows.is_changed() handles cases where a window was resized .add_plugins(( + ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), ExtractComponentPlugin::::default(), VisibilityPlugin, @@ -199,6 +197,16 @@ impl Msaa { } } +/// If this component is added to a camera, the camera will use an intermediate "high dynamic range" render texture. +/// This allows rendering with a wider range of lighting values. However, this does *not* affect +/// whether the camera will render with hdr display output (which bevy does not support currently) +/// and only affects the intermediate render texture. +#[derive( + Component, Default, Copy, Clone, ExtractComponent, Reflect, PartialEq, Eq, Hash, Debug, +)] +#[reflect(Component, Default, PartialEq, Hash, Debug)] +pub struct Hdr; + /// An identifier for a view that is stable across frames. /// /// We can't use [`Entity`] for this because render world entities aren't @@ -262,34 +270,36 @@ impl RetainedViewEntity { pub struct ExtractedView { /// The entity in the main world corresponding to this render world view. pub retained_view_entity: RetainedViewEntity, - /// Typically a right-handed projection matrix, one of either: + /// Typically a column-major 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 ⎦ + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ /// ``` /// /// Orthographic /// ```text /// w = right - left /// h = top - bottom - /// d = near - far + /// d = far - near /// 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 ⎦ + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ /// ``` /// /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// /// Custom projections are also possible however. pub clip_from_view: Mat4, pub world_from_view: GlobalTransform, @@ -306,7 +316,7 @@ pub struct ExtractedView { impl ExtractedView { /// Creates a 3D rangefinder for a view pub fn rangefinder3d(&self) -> ViewRangefinder3d { - ViewRangefinder3d::from_world_from_view(&self.world_from_view.compute_matrix()) + ViewRangefinder3d::from_world_from_view(&self.world_from_view.to_matrix()) } } @@ -529,34 +539,36 @@ 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: + /// Typically a column-major 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 ⎦ + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ /// ``` /// /// Orthographic /// ```text /// w = right - left /// h = top - bottom - /// d = near - far + /// d = far - near /// 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 ⎦ + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ /// ``` /// /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// /// Custom projections are also possible however. pub clip_from_view: Mat4, pub view_from_clip: Mat4, @@ -922,7 +934,7 @@ pub fn prepare_view_uniforms( } let view_from_clip = clip_from_view.inverse(); - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let view_from_world = world_from_view.inverse(); let clip_from_world = if temporal_jitter.is_some() { diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 317de2eb88..7b14bab9e1 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -19,33 +19,35 @@ struct View { world_from_clip: mat4x4, world_from_view: mat4x4, view_from_world: mat4x4, - // Typically a right-handed projection matrix, one of either: + // Typically a column-major 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 ⎦ + // ⎡ f / aspect 0 0 0 ⎤ + // ⎢ 0 f 0 0 ⎥ + // ⎢ 0 0 0 near ⎥ + // ⎣ 0 0 -1 0 ⎦ // ``` // // Orthographic // ``` // w = right - left // h = top - bottom - // d = near - far + // d = far - near // 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 ⎦ + // ⎡ 2 / w 0 0 cw / w ⎤ + // ⎢ 0 2 / h 0 ch / h ⎥ + // ⎢ 0 0 1 / d far / d ⎥ + // ⎣ 0 0 0 1 ⎦ // ``` // // `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + // + // Wgsl matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` // // Custom projections are also possible however. clip_from_view: mat4x4, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 3a0772b687..13b8ac74d4 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -3,8 +3,8 @@ mod render_layers; use core::any::TypeId; -use bevy_ecs::component::HookContext; use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::lifecycle::HookContext; use bevy_ecs::world::DeferredWorld; use derive_more::derive::{Deref, DerefMut}; pub use range::*; @@ -129,7 +129,7 @@ impl InheritedVisibility { /// A bucket into which we group entities for the purposes of visibility. /// -/// Bevy's various rendering subsystems (3D, 2D, UI, etc.) want to be able to +/// Bevy's various rendering subsystems (3D, 2D, etc.) want to be able to /// quickly winnow the set of entities to only those that the subsystem is /// tasked with rendering, to avoid spending time examining irrelevant entities. /// At the same time, Bevy wants the [`check_visibility`] system to determine diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 2559d3b8d2..80f89ce936 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -10,9 +10,9 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, + lifecycle::RemovedComponents, query::{Changed, With}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs as _, system::{Local, Query, Res, ResMut}, diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_render/src/view/visibility/render_layers.rs index a5a58453e8..b39ecb215c 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_render/src/view/visibility/render_layers.rs @@ -7,18 +7,14 @@ pub const DEFAULT_LAYERS: &RenderLayers = &RenderLayers::layer(0); /// An identifier for a rendering layer. pub type Layer = usize; -/// Describes which rendering layers an entity belongs to. +/// Defines which rendering layers an entity belongs to. /// -/// Cameras with this component will only render entities with intersecting -/// layers. +/// A camera renders an entity only when their render layers intersect. /// -/// Entities may belong to one or more layers, or no layer at all. +/// The [`Default`] instance of `RenderLayers` contains layer `0`, the first layer. Entities +/// without this component also belong to layer `0`. /// -/// The [`Default`] instance of `RenderLayers` contains layer `0`, the first layer. -/// -/// An entity with this component without any layers is invisible. -/// -/// Entities without this component belong to layer `0`. +/// An empty `RenderLayers` makes the entity invisible. #[derive(Component, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] #[reflect(Component, Default, PartialEq, Debug, Clone)] pub struct RenderLayers(SmallVec<[u64; INLINE_BLOCKS]>); diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 4c8d86d040..657106d5a0 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -1,12 +1,13 @@ use crate::{ render_resource::{SurfaceTexture, TextureView}, renderer::{RenderAdapter, RenderDevice, RenderInstance}, - Extract, ExtractSchedule, Render, RenderApp, RenderSystems, WgpuWrapper, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; use bevy_platform::collections::HashSet; use bevy_utils::default; +use bevy_utils::WgpuWrapper; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 854a6bc064..33b76d269d 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -17,7 +17,7 @@ use crate::{ }; use alloc::{borrow::Cow, sync::Arc}; use bevy_app::{First, Plugin, Update}; -use bevy_asset::{load_internal_asset, weak_handle, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ entity::EntityHashMap, event::event_update_system, prelude::*, system::SystemState, @@ -39,7 +39,7 @@ use std::{ use tracing::{error, info, warn}; use wgpu::{CommandEncoder, Extent3d, TextureFormat}; -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ScreenshotCaptured(pub Image); @@ -122,7 +122,7 @@ struct RenderScreenshotsPrepared(EntityHashMap); struct RenderScreenshotsSender(Sender<(Entity, Image)>); /// Saves the captured screenshot to disk at the provided path. -pub fn save_to_disk(path: impl AsRef) -> impl FnMut(Trigger) { +pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { let path = path.as_ref().to_owned(); move |trigger| { let img = trigger.event().deref().clone(); @@ -392,9 +392,6 @@ fn prepare_screenshot_state( pub struct ScreenshotPlugin; -const SCREENSHOT_SHADER_HANDLE: Handle = - weak_handle!("c31753d6-326a-47cb-a359-65c97a471fda"); - impl Plugin for ScreenshotPlugin { fn build(&self, app: &mut bevy_app::App) { app.add_systems( @@ -403,21 +400,16 @@ impl Plugin for ScreenshotPlugin { .after(event_update_system) .before(ApplyDeferred), ) - .add_systems(Update, trigger_screenshots) .register_type::() .register_type::(); - load_internal_asset!( - app, - SCREENSHOT_SHADER_HANDLE, - "screenshot.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "screenshot.wgsl"); } fn finish(&self, app: &mut bevy_app::App) { let (tx, rx) = std::sync::mpsc::channel(); - app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); + app.add_systems(Update, trigger_screenshots) + .insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -441,6 +433,7 @@ impl Plugin for ScreenshotPlugin { #[derive(Resource)] pub struct ScreenshotToScreenPipeline { pub bind_group_layout: BindGroupLayout, + pub shader: Handle, } impl FromWorld for ScreenshotToScreenPipeline { @@ -455,7 +448,12 @@ impl FromWorld for ScreenshotToScreenPipeline { ), ); - Self { bind_group_layout } + let shader = load_embedded_asset!(render_world, "screenshot.wgsl"); + + Self { + bind_group_layout, + shader, + } } } @@ -470,7 +468,7 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { buffers: vec![], shader_defs: vec![], entry_point: Cow::Borrowed("vs_main"), - shader: SCREENSHOT_SHADER_HANDLE, + shader: self.shader.clone(), }, primitive: wgpu::PrimitiveState { cull_mode: Some(wgpu::Face::Back), @@ -479,7 +477,7 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { depth_stencil: None, multisample: Default::default(), fragment: Some(FragmentState { - shader: SCREENSHOT_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: Cow::Borrowed("fs_main"), shader_defs: vec![], targets: vec![Some(wgpu::ColorTargetState { diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 3bb913c859..78de17e26f 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_scene" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides scene functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -19,15 +19,15 @@ serialize = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev", optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev", optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index c9e594107e..ee0b15847a 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -350,7 +350,7 @@ impl<'w> DynamicSceneBuilder<'w> { let original_world_dqf_id = self .original_world .components() - .get_resource_id(TypeId::of::()); + .get_valid_resource_id(TypeId::of::()); let type_registry = self.original_world.resource::().read(); diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index a507a58aaf..9b0845f80f 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Provides scene definition, instantiation and serialization/deserialization. diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 1d684c9dac..2293beef1e 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -93,7 +93,7 @@ impl Scene { type_registry .get(type_id) .ok_or_else(|| SceneSpawnError::UnregisteredType { - std_type_name: component_info.name().to_string(), + std_type_name: component_info.name(), })?; let reflect_resource = registration.data::().ok_or_else(|| { SceneSpawnError::UnregisteredResource { @@ -133,7 +133,7 @@ impl Scene { let registration = type_registry .get(component_info.type_id().unwrap()) .ok_or_else(|| SceneSpawnError::UnregisteredType { - std_type_name: component_info.name().to_string(), + std_type_name: component_info.name(), })?; let reflect_component = registration.data::().ok_or_else(|| { diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 3bf8ca9f64..71cd848751 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -2,7 +2,7 @@ use crate::{DynamicScene, Scene}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::{ entity::{Entity, EntityHashMap}, - event::{Event, EventCursor, Events}, + event::{EntityEvent, Event, EventCursor, Events}, hierarchy::ChildOf, reflect::AppTypeRegistry, resource::Resource, @@ -10,6 +10,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::Reflect; +use bevy_utils::prelude::DebugName; use thiserror::Error; use uuid::Uuid; @@ -22,10 +23,10 @@ use bevy_ecs::{ }; /// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. /// -/// See also [`Trigger`], [`SceneSpawner::instance_is_ready`]. +/// See also [`On`], [`SceneSpawner::instance_is_ready`]. /// -/// [`Trigger`]: bevy_ecs::observer::Trigger -#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, Reflect)] +/// [`On`]: bevy_ecs::observer::On +#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, EntityEvent, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { /// Instance which has been spawned. @@ -79,6 +80,7 @@ pub struct SceneSpawner { scenes_to_despawn: Vec>, instances_to_despawn: Vec, scenes_with_parent: Vec<(InstanceId, Entity)>, + instances_ready: Vec<(InstanceId, Option)>, } /// Errors that can occur when spawning a scene. @@ -104,7 +106,7 @@ pub enum SceneSpawnError { )] UnregisteredType { /// The [type name](std::any::type_name) for the unregistered type. - std_type_name: String, + std_type_name: DebugName, }, /// Scene contains an unregistered type which has a `TypePath`. #[error( @@ -337,8 +339,9 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); } } Err(SceneSpawnError::NonExistentScene { .. }) => { @@ -362,8 +365,9 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); } } Err(SceneSpawnError::NonExistentRealScene { .. }) => { @@ -398,12 +402,25 @@ impl SceneSpawner { } } + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, Some(parent))); + } else { + self.scenes_with_parent.push((instance_id, parent)); + } + } + } + + fn trigger_scene_ready_events(&mut self, world: &mut World) { + for (instance_id, parent) in self.instances_ready.drain(..) { + if let Some(parent) = parent { // Defer via commands otherwise SceneSpawner is not available in the observer. world .commands() .trigger_targets(SceneInstanceReady { instance_id }, parent); } else { - self.scenes_with_parent.push((instance_id, parent)); + // Defer via commands otherwise SceneSpawner is not available in the observer. + world.commands().trigger(SceneInstanceReady { instance_id }); } } } @@ -477,6 +494,7 @@ pub fn scene_spawner_system(world: &mut World) { .update_spawned_scenes(world, &updated_spawned_scenes) .unwrap(); scene_spawner.set_scene_instance_parent_sync(world); + scene_spawner.trigger_scene_ready_events(world); }); } @@ -525,7 +543,7 @@ mod tests { use bevy_ecs::{ component::Component, hierarchy::Children, - observer::Trigger, + observer::On, prelude::ReflectComponent, query::With, system::{Commands, Query, Res, ResMut, RunSystemOnce}, @@ -704,10 +722,10 @@ mod tests { .expect("Failed to run dynamic scene builder system.") } - fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) { + fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( - move |trigger: Trigger, + move |trigger: On, scene_spawner: Res, mut trigger_count: ResMut| { assert_eq!( @@ -717,7 +735,7 @@ mod tests { ); assert_eq!( trigger.target(), - scene_entity, + scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); assert!( @@ -756,7 +774,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -775,7 +793,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -799,7 +817,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] @@ -823,7 +841,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml new file mode 100644 index 0000000000..03976dea3f --- /dev/null +++ b/crates/bevy_solari/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "bevy_solari" +version = "0.17.0-dev" +edition = "2024" +description = "Provides raytraced lighting for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ + "std", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } + +# other +bytemuck = { version = "1" } +derive_more = { version = "1", default-features = false, features = ["from"] } +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_solari/LICENSE-APACHE b/crates/bevy_solari/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_solari/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_solari/LICENSE-MIT b/crates/bevy_solari/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_solari/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_solari/README.md b/crates/bevy_solari/README.md new file mode 100644 index 0000000000..089418e60d --- /dev/null +++ b/crates/bevy_solari/README.md @@ -0,0 +1,9 @@ +# Bevy Solari + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Downloads](https://img.shields.io/crates/d/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Docs](https://docs.rs/bevy_solari/badge.svg)](https://docs.rs/bevy_solari/latest/bevy_solari/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) + +![Logo](../../assets/branding/bevy_solari.svg) diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs new file mode 100644 index 0000000000..416c850b04 --- /dev/null +++ b/crates/bevy_solari/src/lib.rs @@ -0,0 +1,53 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + +//! Provides raytraced lighting. +//! +//! See [`SolariPlugin`] for more info. +//! +//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg) +pub mod pathtracer; +pub mod realtime; +pub mod scene; + +/// The solari prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + pub use super::SolariPlugin; + pub use crate::realtime::SolariLighting; + pub use crate::scene::RaytracingMesh3d; +} + +use crate::realtime::SolariLightingPlugin; +use crate::scene::RaytracingScenePlugin; +use bevy_app::{App, Plugin}; +use bevy_render::settings::WgpuFeatures; + +/// An experimental plugin for raytraced lighting. +/// +/// This plugin provides: +/// * [`SolariLightingPlugin`] - Raytraced direct and indirect lighting (indirect lighting not yet implemented). +/// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding. +/// * [`pathtracer::PathtracingPlugin`] - A non-realtime pathtracer for validation purposes. +/// +/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::` to your entities. +pub struct SolariPlugin; + +impl Plugin for SolariPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((RaytracingScenePlugin, SolariLightingPlugin)); + } +} + +impl SolariPlugin { + /// [`WgpuFeatures`] required for this plugin to function. + pub fn required_wgpu_features() -> WgpuFeatures { + WgpuFeatures::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE + | WgpuFeatures::EXPERIMENTAL_RAY_QUERY + | WgpuFeatures::BUFFER_BINDING_ARRAY + | WgpuFeatures::TEXTURE_BINDING_ARRAY + | WgpuFeatures::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY + } +} diff --git a/crates/bevy_solari/src/pathtracer/extract.rs b/crates/bevy_solari/src/pathtracer/extract.rs new file mode 100644 index 0000000000..38f27968a6 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/extract.rs @@ -0,0 +1,33 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use bevy_ecs::{ + change_detection::DetectChanges, + system::{Commands, Query}, + world::Ref, +}; +use bevy_render::{camera::Camera, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_pathtracer( + cameras_3d: Extract< + Query<( + RenderEntity, + &Camera, + Ref, + Option<&Pathtracer>, + )>, + >, + mut commands: Commands, +) { + for (entity, camera, global_transform, pathtracer) in &cameras_3d { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if pathtracer.is_some() && camera.is_active { + let mut pathtracer = pathtracer.unwrap().clone(); + pathtracer.reset |= global_transform.is_changed(); + entity_commands.insert(pathtracer); + } else { + entity_commands.remove::<(Pathtracer, PathtracerAccumulationTexture)>(); + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/mod.rs b/crates/bevy_solari/src/pathtracer/mod.rs new file mode 100644 index 0000000000..1e2cd95ed8 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/mod.rs @@ -0,0 +1,67 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::embedded_asset; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + render_graph::{RenderGraphApp, ViewNodeRunner}, + renderer::RenderDevice, + view::Hdr, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_pathtracer; +use node::PathtracerNode; +use prepare::prepare_pathtracer_accumulation_texture; +use tracing::warn; + +/// Non-realtime pathtracing. +/// +/// This plugin is meant to generate reference screenshots to compare against, +/// and is not intended to be used by games. +pub struct PathtracingPlugin; + +impl Plugin for PathtracingPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "pathtracer.wgsl"); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "PathtracingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + render_app + .add_systems(ExtractSchedule, extract_pathtracer) + .add_systems( + Render, + prepare_pathtracer_accumulation_texture.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::PathtracerNode, + ) + .add_render_graph_edges(Core3d, (Node3d::EndMainPass, node::graph::PathtracerNode)); + } +} + +#[derive(Component, Reflect, Default, Clone)] +#[reflect(Component, Default, Clone)] +#[require(Hdr)] +pub struct Pathtracer { + pub reset: bool, +} diff --git a/crates/bevy_solari/src/pathtracer/node.rs b/crates/bevy_solari/src/pathtracer/node.rs new file mode 100644 index 0000000000..30031c1d51 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/node.rs @@ -0,0 +1,134 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{texture_storage_2d, uniform_buffer}, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, ImageSubresourceRange, PipelineCache, + ShaderStages, StorageTextureAccess, TextureFormat, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct PathtracerNode; +} + +pub struct PathtracerNode { + bind_group_layout: BindGroupLayout, + pipeline: CachedComputePipelineId, +} + +impl ViewNode for PathtracerNode { + type ViewQuery = ( + &'static Pathtracer, + &'static PathtracerAccumulationTexture, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (pathtracer, accumulation_texture, camera, view_target, view_uniform_offset): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let (Some(pipeline), Some(scene_bindings), Some(viewport), Some(view_uniforms)) = ( + pipeline_cache.get_compute_pipeline(self.pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_uniforms.uniforms.binding(), + ) else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "pathtracer_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + &accumulation_texture.0.default_view, + view_target.get_unsampled_color_attachment().view, + view_uniforms, + )), + ); + + let command_encoder = render_context.command_encoder(); + + if pathtracer.reset { + command_encoder.clear_texture( + &accumulation_texture.0.texture, + &ImageSubresourceRange::default(), + ); + } + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("pathtracer"), + timestamp_writes: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + Ok(()) + } +} + +impl FromWorld for PathtracerNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "pathtracer_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadWrite), + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(true), + ), + ), + ); + + let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("pathtracer_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![], + shader: load_embedded_asset!(world, "pathtracer.wgsl"), + shader_defs: vec![], + entry_point: "pathtrace".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + pipeline, + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl new file mode 100644 index 0000000000..c67b53e58e --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -0,0 +1,78 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::utils::{rand_f, rand_vec2f} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::sampling::{sample_random_light, sample_cosine_hemisphere} +#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} + +@group(1) @binding(0) var accumulation_texture: texture_storage_2d; +@group(1) @binding(1) var view_output: texture_storage_2d; +@group(1) @binding(2) var view: View; + +@compute @workgroup_size(8, 8, 1) +fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { + return; + } + + let old_color = textureLoad(accumulation_texture, global_id.xy); + + // Setup RNG + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + let frame_index = u32(old_color.a) * 5782582u; + var rng = pixel_index + frame_index; + + // Shoot the first ray from the camera + let pixel_center = vec2(global_id.xy) + 0.5; + let jitter = rand_vec2f(&rng) - 0.5; + let pixel_uv = (pixel_center + jitter) / view.viewport.zw; + let pixel_ndc = (pixel_uv * 2.0) - 1.0; + let primary_ray_target = view.world_from_clip * vec4(pixel_ndc.x, -pixel_ndc.y, 1.0, 1.0); + var ray_origin = view.world_position; + var ray_direction = normalize((primary_ray_target.xyz / primary_ray_target.w) - ray_origin); + var ray_t_min = 0.0; + + // Path trace + var radiance = vec3(0.0); + var throughput = vec3(1.0); + loop { + let ray_hit = trace_ray(ray_origin, ray_direction, ray_t_min, RAY_T_MAX, RAY_FLAG_NONE); + if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE { + let ray_hit = resolve_ray_hit_full(ray_hit); + + // Evaluate material BRDF + let diffuse_brdf = ray_hit.material.base_color / PI; + + // Use emissive only on the first ray (coming from the camera) + if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; } + + // Sample direct lighting + radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); + + // Sample new ray direction from the material BRDF for next bounce + ray_direction = sample_cosine_hemisphere(ray_hit.world_normal, &rng); + + // Update other variables for next bounce + ray_origin = ray_hit.world_position; + ray_t_min = RAY_T_MIN; + + // Update throughput for next bounce + let cos_theta = dot(-ray_direction, ray_hit.world_normal); + let cosine_hemisphere_pdf = cos_theta / PI; // Weight for the next bounce because we importance sampled the diffuse BRDF for the next ray direction + throughput *= (diffuse_brdf * cos_theta) / cosine_hemisphere_pdf; + + // Russian roulette for early termination + let p = luminance(throughput); + if rand_f(&rng) > p { break; } + throughput /= p; + } else { break; } + } + + // Camera exposure + radiance *= view.exposure; + + // Accumulation over time via running average + let new_color = mix(old_color.rgb, radiance, 1.0 / (old_color.a + 1.0)); + textureStore(accumulation_texture, global_id.xy, vec4(new_color, old_color.a + 1.0)); + textureStore(view_output, global_id.xy, vec4(new_color, 1.0)); +} diff --git a/crates/bevy_solari/src/pathtracer/prepare.rs b/crates/bevy_solari/src/pathtracer/prepare.rs new file mode 100644 index 0000000000..7ef4733124 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/prepare.rs @@ -0,0 +1,52 @@ +use super::Pathtracer; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, +}; + +#[derive(Component)] +pub struct PathtracerAccumulationTexture(pub CachedTexture); + +pub fn prepare_pathtracer_accumulation_texture( + query: Query<(Entity, &ExtractedCamera), With>, + mut texture_cache: ResMut, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera) in &query { + let Some(viewport) = camera.physical_viewport_size else { + continue; + }; + + let descriptor = TextureDescriptor { + label: Some("pathtracer_accumulation_texture"), + size: Extent3d { + width: viewport.x, + height: viewport.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::STORAGE_BINDING, + view_formats: &[], + }; + + commands + .entity(entity) + .insert(PathtracerAccumulationTexture( + texture_cache.get(&render_device, descriptor), + )); + } +} diff --git a/crates/bevy_solari/src/realtime/extract.rs b/crates/bevy_solari/src/realtime/extract.rs new file mode 100644 index 0000000000..8e80f02327 --- /dev/null +++ b/crates/bevy_solari/src/realtime/extract.rs @@ -0,0 +1,27 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use bevy_ecs::system::{Commands, ResMut}; +use bevy_pbr::deferred::SkipDeferredLighting; +use bevy_render::{camera::Camera, sync_world::RenderEntity, MainWorld}; + +pub fn extract_solari_lighting(mut main_world: ResMut, mut commands: Commands) { + let mut cameras_3d = main_world.query::<(RenderEntity, &Camera, Option<&mut SolariLighting>)>(); + + for (entity, camera, mut solari_lighting) in cameras_3d.iter_mut(&mut main_world) { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if solari_lighting.is_some() && camera.is_active { + entity_commands.insert(( + solari_lighting.as_deref().unwrap().clone(), + SkipDeferredLighting, + )); + solari_lighting.as_mut().unwrap().reset = false; + } else { + entity_commands.remove::<( + SolariLighting, + SolariLightingResources, + SkipDeferredLighting, + )>(); + } + } +} diff --git a/crates/bevy_solari/src/realtime/mod.rs b/crates/bevy_solari/src/realtime/mod.rs new file mode 100644 index 0000000000..9308ab5cf8 --- /dev/null +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -0,0 +1,91 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::embedded_asset; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass}, +}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_pbr::DefaultOpaqueRendererMethod; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + load_shader_library, + render_graph::{RenderGraphApp, ViewNodeRunner}, + renderer::RenderDevice, + view::Hdr, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_solari_lighting; +use node::SolariLightingNode; +use prepare::prepare_solari_lighting_resources; +use tracing::warn; + +pub struct SolariLightingPlugin; + +impl Plugin for SolariLightingPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "restir_di.wgsl"); + load_shader_library!(app, "reservoir.wgsl"); + + app.register_type::() + .insert_resource(DefaultOpaqueRendererMethod::deferred()); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "SolariLightingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + render_app + .add_systems(ExtractSchedule, extract_solari_lighting) + .add_systems( + Render, + prepare_solari_lighting_resources.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::SolariLightingNode, + ) + .add_render_graph_edges( + Core3d, + (Node3d::EndMainPass, node::graph::SolariLightingNode), + ); + } +} + +/// A component for a 3d camera entity to enable the Solari raytraced lighting system. +/// +/// Must be used with `CameraMainTextureUsages::default().with(TextureUsages::STORAGE_BINDING)`, and +/// `Msaa::Off`. +#[derive(Component, Reflect, Clone)] +#[reflect(Component, Default, Clone)] +#[require(Hdr, DeferredPrepass, DepthPrepass, MotionVectorPrepass)] +pub struct SolariLighting { + /// Set to true to delete the saved temporal history (past frames). + /// + /// Useful for preventing ghosting when the history is no longer + /// representative of the current frame, such as in sudden camera cuts. + /// + /// After setting this to true, it will automatically be toggled + /// back to false at the end of the frame. + pub reset: bool, +} + +impl Default for SolariLighting { + fn default() -> Self { + Self { + reset: true, // No temporal history on the first frame + } + } +} diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs new file mode 100644 index 0000000000..6060bb3c15 --- /dev/null +++ b/crates/bevy_solari/src/realtime/node.rs @@ -0,0 +1,200 @@ +use super::{prepare::SolariLightingResources, SolariLighting}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{ + storage_buffer_sized, texture_2d, texture_depth_2d, texture_storage_2d, uniform_buffer, + }, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, PipelineCache, PushConstantRange, + ShaderStages, StorageTextureAccess, TextureSampleType, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct SolariLightingNode; +} + +pub struct SolariLightingNode { + bind_group_layout: BindGroupLayout, + initial_and_temporal_pipeline: CachedComputePipelineId, + spatial_and_shade_pipeline: CachedComputePipelineId, +} + +impl ViewNode for SolariLightingNode { + type ViewQuery = ( + &'static SolariLighting, + &'static SolariLightingResources, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + solari_lighting, + solari_lighting_resources, + camera, + view_target, + view_prepass_textures, + view_uniform_offset, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let frame_count = world.resource::(); + let ( + Some(initial_and_temporal_pipeline), + Some(spatial_and_shade_pipeline), + Some(scene_bindings), + Some(viewport), + Some(gbuffer), + Some(depth_buffer), + Some(motion_vectors), + Some(view_uniforms), + ) = ( + pipeline_cache.get_compute_pipeline(self.initial_and_temporal_pipeline), + pipeline_cache.get_compute_pipeline(self.spatial_and_shade_pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_prepass_textures.deferred_view(), + view_prepass_textures.depth_view(), + view_prepass_textures.motion_vectors_view(), + view_uniforms.uniforms.binding(), + ) + else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "solari_lighting_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + view_target.get_unsampled_color_attachment().view, + solari_lighting_resources.reservoirs_a.as_entire_binding(), + solari_lighting_resources.reservoirs_b.as_entire_binding(), + gbuffer, + depth_buffer, + motion_vectors, + view_uniforms, + )), + ); + + // Choice of number here is arbitrary + let frame_index = frame_count.0.wrapping_mul(5782582); + + let diagnostics = render_context.diagnostic_recorder(); + let command_encoder = render_context.command_encoder(); + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("solari_lighting"), + timestamp_writes: None, + }); + let pass_span = diagnostics.pass_span(&mut pass, "solari_lighting"); + + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + + pass.set_pipeline(initial_and_temporal_pipeline); + pass.set_push_constants( + 0, + bytemuck::cast_slice(&[frame_index, solari_lighting.reset as u32]), + ); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass.set_pipeline(spatial_and_shade_pipeline); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + pass_span.end(&mut pass); + + Ok(()) + } +} + +impl FromWorld for SolariLightingNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "solari_lighting_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + storage_buffer_sized(false, None), + storage_buffer_sized(false, None), + texture_2d(TextureSampleType::Uint), + texture_depth_2d(), + texture_2d(TextureSampleType::Float { filterable: true }), + uniform_buffer::(true), + ), + ), + ); + + let initial_and_temporal_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_initial_and_temporal_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_di.wgsl"), + shader_defs: vec![], + entry_point: "initial_and_temporal".into(), + zero_initialize_workgroup_memory: false, + }); + + let spatial_and_shade_pipeline = + pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("solari_lighting_spatial_and_shade_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![PushConstantRange { + stages: ShaderStages::COMPUTE, + range: 0..8, + }], + shader: load_embedded_asset!(world, "restir_di.wgsl"), + shader_defs: vec![], + entry_point: "spatial_and_shade".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + initial_and_temporal_pipeline, + spatial_and_shade_pipeline, + } + } +} diff --git a/crates/bevy_solari/src/realtime/prepare.rs b/crates/bevy_solari/src/realtime/prepare.rs new file mode 100644 index 0000000000..4f153bf0dc --- /dev/null +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -0,0 +1,65 @@ +use super::SolariLighting; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res}, +}; +use bevy_math::UVec2; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{Buffer, BufferDescriptor, BufferUsages}, + renderer::RenderDevice, +}; + +/// Size of a Reservoir shader struct in bytes. +const RESERVOIR_STRUCT_SIZE: u64 = 32; + +/// Internal rendering resources used for Solari lighting. +#[derive(Component)] +pub struct SolariLightingResources { + pub reservoirs_a: Buffer, + pub reservoirs_b: Buffer, + pub view_size: UVec2, +} + +pub fn prepare_solari_lighting_resources( + query: Query< + (Entity, &ExtractedCamera, Option<&SolariLightingResources>), + With, + >, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera, solari_lighting_resources) in &query { + let Some(view_size) = camera.physical_viewport_size else { + continue; + }; + + if solari_lighting_resources.map(|r| r.view_size) == Some(view_size) { + continue; + } + + let size = (view_size.x * view_size.y) as u64 * RESERVOIR_STRUCT_SIZE; + + let reservoirs_a = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_reservoirs_a"), + size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + let reservoirs_b = render_device.create_buffer(&BufferDescriptor { + label: Some("solari_lighting_reservoirs_b"), + size, + usage: BufferUsages::STORAGE, + mapped_at_creation: false, + }); + + commands.entity(entity).insert(SolariLightingResources { + reservoirs_a, + reservoirs_b, + view_size, + }); + } +} diff --git a/crates/bevy_solari/src/realtime/reservoir.wgsl b/crates/bevy_solari/src/realtime/reservoir.wgsl new file mode 100644 index 0000000000..08a7e26f7c --- /dev/null +++ b/crates/bevy_solari/src/realtime/reservoir.wgsl @@ -0,0 +1,30 @@ +// https://intro-to-restir.cwyman.org/presentations/2023ReSTIR_Course_Notes.pdf + +#define_import_path bevy_solari::reservoir + +#import bevy_solari::sampling::LightSample + +const NULL_RESERVOIR_SAMPLE = 0xFFFFFFFFu; + +// Don't adjust the size of this struct without also adjusting RESERVOIR_STRUCT_SIZE. +struct Reservoir { + sample: LightSample, + weight_sum: f32, + confidence_weight: f32, + unbiased_contribution_weight: f32, + _padding: f32, +} + +fn empty_reservoir() -> Reservoir { + return Reservoir( + LightSample(vec2(NULL_RESERVOIR_SAMPLE, 0u), vec2(0.0)), + 0.0, + 0.0, + 0.0, + 0.0 + ); +} + +fn reservoir_valid(reservoir: Reservoir) -> bool { + return reservoir.sample.light_id.x != NULL_RESERVOIR_SAMPLE; +} diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl new file mode 100644 index 0000000000..511fd63d12 --- /dev/null +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -0,0 +1,117 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ +#import bevy_pbr::utils::{rand_f, octahedral_decode} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::reservoir::{Reservoir, empty_reservoir, reservoir_valid} +#import bevy_solari::sampling::{generate_random_light_sample, calculate_light_contribution, trace_light_visibility} + +@group(1) @binding(0) var view_output: texture_storage_2d; +@group(1) @binding(1) var reservoirs_a: array; +@group(1) @binding(2) var reservoirs_b: array; +@group(1) @binding(3) var gbuffer: texture_2d; +@group(1) @binding(4) var depth_buffer: texture_depth_2d; +@group(1) @binding(5) var motion_vectors: texture_2d; +@group(1) @binding(6) var view: View; +struct PushConstants { frame_index: u32, reset: u32 } +var constants: PushConstants; + +const INITIAL_SAMPLES = 32u; +const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; +const CONFIDENCE_WEIGHT_CAP = 20.0 * f32(INITIAL_SAMPLES); + +@compute @workgroup_size(8, 8, 1) +fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + reservoirs_b[pixel_index] = empty_reservoir(); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + + let initial_reservoir = generate_initial_reservoir(world_position, world_normal, diffuse_brdf, &rng); + + reservoirs_b[pixel_index] = initial_reservoir; +} + +@compute @workgroup_size(8, 8, 1) +fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { return; } + + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + var rng = pixel_index + constants.frame_index; + + let depth = textureLoad(depth_buffer, global_id.xy, 0); + if depth == 0.0 { + reservoirs_a[pixel_index] = empty_reservoir(); + textureStore(view_output, global_id.xy, vec4(vec3(0.0), 1.0)); + return; + } + let gpixel = textureLoad(gbuffer, global_id.xy, 0); + let world_position = reconstruct_world_position(global_id.xy, depth); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; + let emissive = rgb9e5_to_vec3_(gpixel.g); + + let input_reservoir = reservoirs_b[pixel_index]; + + var radiance = vec3(0.0); + if reservoir_valid(input_reservoir) { + radiance = calculate_light_contribution(input_reservoir.sample, world_position, world_normal).radiance; + } + + reservoirs_a[pixel_index] = input_reservoir; + + var pixel_color = radiance * input_reservoir.unbiased_contribution_weight; + pixel_color *= view.exposure; + pixel_color *= diffuse_brdf; + pixel_color += emissive; + textureStore(view_output, global_id.xy, vec4(pixel_color, 1.0)); +} + +fn generate_initial_reservoir(world_position: vec3, world_normal: vec3, diffuse_brdf: vec3, rng: ptr) -> Reservoir{ + var reservoir = empty_reservoir(); + var reservoir_target_function = 0.0; + for (var i = 0u; i < INITIAL_SAMPLES; i++) { + let light_sample = generate_random_light_sample(rng); + + let mis_weight = 1.0 / f32(INITIAL_SAMPLES); + let light_contribution = calculate_light_contribution(light_sample, world_position, world_normal); + let target_function = luminance(light_contribution.radiance * diffuse_brdf); + let resampling_weight = mis_weight * (target_function * light_contribution.inverse_pdf); + + reservoir.weight_sum += resampling_weight; + + if rand_f(rng) < resampling_weight / reservoir.weight_sum { + reservoir.sample = light_sample; + reservoir_target_function = target_function; + } + } + + if reservoir_valid(reservoir) { + let inverse_target_function = select(0.0, 1.0 / reservoir_target_function, reservoir_target_function > 0.0); + reservoir.unbiased_contribution_weight = reservoir.weight_sum * inverse_target_function; + reservoir.unbiased_contribution_weight *= trace_light_visibility(reservoir.sample, world_position); + } + + reservoir.confidence_weight = f32(INITIAL_SAMPLES); + return reservoir; +} + +fn reconstruct_world_position(pixel_id: vec2, depth: f32) -> vec3 { + let uv = (vec2(pixel_id) + 0.5) / view.viewport.zw; + let xy_ndc = (uv - vec2(0.5)) * vec2(2.0, -2.0); + let world_pos = view.world_from_clip * vec4(xy_ndc, depth, 1.0); + return world_pos.xyz / world_pos.w; +} diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs new file mode 100644 index 0000000000..889efb538e --- /dev/null +++ b/crates/bevy_solari/src/scene/binder.rs @@ -0,0 +1,366 @@ +use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d}; +use bevy_asset::{AssetId, Handle}; +use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_ecs::{ + resource::Resource, + system::{Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_math::{ops::cos, Mat4, Vec3}; +use bevy_pbr::{ExtractedDirectionalLight, MeshMaterial3d, StandardMaterial}; +use bevy_platform::{collections::HashMap, hash::FixedHasher}; +use bevy_render::{ + mesh::allocator::MeshAllocator, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{FallbackImage, GpuImage}, +}; +use bevy_transform::components::GlobalTransform; +use core::{f32::consts::TAU, hash::Hash, num::NonZeroU32, ops::Deref}; + +const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap(); +const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap(); + +/// Average angular diameter of the sun as seen from earth. +/// +const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842; + +#[derive(Resource)] +pub struct RaytracingSceneBindings { + pub bind_group: Option, + pub bind_group_layout: BindGroupLayout, +} + +pub fn prepare_raytracing_scene_bindings( + instances_query: Query<( + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + directional_lights_query: Query<&ExtractedDirectionalLight>, + mesh_allocator: Res, + blas_manager: Res, + material_assets: Res, + texture_assets: Res>, + fallback_texture: Res, + render_device: Res, + render_queue: Res, + mut raytracing_scene_bindings: ResMut, +) { + raytracing_scene_bindings.bind_group = None; + + if instances_query.iter().len() == 0 { + return; + } + + let mut vertex_buffers = CachedBindingArray::new(); + let mut index_buffers = CachedBindingArray::new(); + let mut textures = CachedBindingArray::new(); + let mut samplers = Vec::new(); + let mut materials = StorageBufferList::::default(); + let mut tlas = TlasPackage::new(render_device.wgpu_device().create_tlas( + &CreateTlasDescriptor { + label: Some("tlas"), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + max_instances: instances_query.iter().len() as u32, + }, + )); + let mut transforms = StorageBufferList::::default(); + let mut geometry_ids = StorageBufferList::::default(); + let mut material_ids = StorageBufferList::::default(); + let mut light_sources = StorageBufferList::::default(); + let mut directional_lights = StorageBufferList::::default(); + + let mut material_id_map: HashMap, u32, FixedHasher> = + HashMap::default(); + let mut material_id = 0; + let mut process_texture = |texture_handle: &Option>| -> Option { + match texture_handle { + Some(texture_handle) => match texture_assets.get(texture_handle.id()) { + Some(texture) => { + let (texture_id, is_new) = + textures.push_if_absent(texture.texture_view.deref(), texture_handle.id()); + if is_new { + samplers.push(texture.sampler.deref()); + } + Some(texture_id) + } + None => None, + }, + None => Some(u32::MAX), + } + }; + for (asset_id, material) in material_assets.iter() { + let Some(base_color_texture_id) = process_texture(&material.base_color_texture) else { + continue; + }; + let Some(normal_map_texture_id) = process_texture(&material.normal_map_texture) else { + continue; + }; + let Some(emissive_texture_id) = process_texture(&material.emissive_texture) else { + continue; + }; + + materials.get_mut().push(GpuMaterial { + base_color: material.base_color.to_linear(), + emissive: material.emissive, + base_color_texture_id, + normal_map_texture_id, + emissive_texture_id, + _padding: Default::default(), + }); + + material_id_map.insert(*asset_id, material_id); + material_id += 1; + } + + if material_id == 0 { + return; + } + + if textures.is_empty() { + textures.vec.push(fallback_texture.d2.texture_view.deref()); + samplers.push(fallback_texture.d2.sampler.deref()); + } + + let mut instance_id = 0; + for (mesh, material, transform) in &instances_query { + let Some(blas) = blas_manager.get(&mesh.id()) else { + continue; + }; + let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh.id()) else { + continue; + }; + let Some(index_slice) = mesh_allocator.mesh_index_slice(&mesh.id()) else { + continue; + }; + let Some(material_id) = material_id_map.get(&material.id()).copied() else { + continue; + }; + let Some(material) = materials.get().get(material_id as usize) else { + continue; + }; + + let transform = transform.to_matrix(); + *tlas.get_mut_single(instance_id).unwrap() = Some(TlasInstance::new( + blas, + tlas_transform(&transform), + Default::default(), + 0xFF, + )); + + transforms.get_mut().push(transform); + + let (vertex_buffer_id, _) = vertex_buffers.push_if_absent( + vertex_slice.buffer.as_entire_buffer_binding(), + vertex_slice.buffer.id(), + ); + let (index_buffer_id, _) = index_buffers.push_if_absent( + index_slice.buffer.as_entire_buffer_binding(), + index_slice.buffer.id(), + ); + + geometry_ids.get_mut().push(GpuInstanceGeometryIds { + vertex_buffer_id, + vertex_buffer_offset: vertex_slice.range.start, + index_buffer_id, + index_buffer_offset: index_slice.range.start, + }); + + material_ids.get_mut().push(material_id); + + if material.emissive != LinearRgba::BLACK { + light_sources + .get_mut() + .push(GpuLightSource::new_emissive_mesh_light( + instance_id as u32, + (index_slice.range.len() / 3) as u32, + )); + } + + instance_id += 1; + } + + if instance_id == 0 { + return; + } + + for directional_light in &directional_lights_query { + let directional_lights = directional_lights.get_mut(); + let directional_light_id = directional_lights.len() as u32; + + directional_lights.push(GpuDirectionalLight::new(directional_light)); + + light_sources + .get_mut() + .push(GpuLightSource::new_directional_light(directional_light_id)); + } + + materials.write_buffer(&render_device, &render_queue); + transforms.write_buffer(&render_device, &render_queue); + geometry_ids.write_buffer(&render_device, &render_queue); + material_ids.write_buffer(&render_device, &render_queue); + light_sources.write_buffer(&render_device, &render_queue); + directional_lights.write_buffer(&render_device, &render_queue); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_tlas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&[], [&tlas]); + render_queue.submit([command_encoder.finish()]); + + raytracing_scene_bindings.bind_group = Some(render_device.create_bind_group( + "raytracing_scene_bind_group", + &raytracing_scene_bindings.bind_group_layout, + &BindGroupEntries::sequential(( + vertex_buffers.as_slice(), + index_buffers.as_slice(), + textures.as_slice(), + samplers.as_slice(), + materials.binding().unwrap(), + tlas.as_binding(), + transforms.binding().unwrap(), + geometry_ids.binding().unwrap(), + material_ids.binding().unwrap(), + light_sources.binding().unwrap(), + directional_lights.binding().unwrap(), + )), + )); +} + +impl FromWorld for RaytracingSceneBindings { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + Self { + bind_group: None, + bind_group_layout: render_device.create_bind_group_layout( + "raytracing_scene_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + texture_2d(TextureSampleType::Float { filterable: true }) + .count(MAX_TEXTURE_COUNT), + sampler(SamplerBindingType::Filtering).count(MAX_TEXTURE_COUNT), + storage_buffer_read_only_sized(false, None), + acceleration_structure(), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + } + } +} + +struct CachedBindingArray { + map: HashMap, + vec: Vec, +} + +impl CachedBindingArray { + fn new() -> Self { + Self { + map: HashMap::default(), + vec: Vec::default(), + } + } + + fn push_if_absent(&mut self, item: T, item_id: I) -> (u32, bool) { + let mut is_new = false; + let i = *self.map.entry(item_id).or_insert_with(|| { + is_new = true; + let i = self.vec.len() as u32; + self.vec.push(item); + i + }); + (i, is_new) + } + + fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } +} + +type StorageBufferList = StorageBuffer>; + +#[derive(ShaderType)] +struct GpuInstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +#[derive(ShaderType)] +struct GpuMaterial { + base_color: LinearRgba, + emissive: LinearRgba, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +#[derive(ShaderType)] +struct GpuLightSource { + kind: u32, + id: u32, +} + +impl GpuLightSource { + fn new_emissive_mesh_light(instance_id: u32, triangle_count: u32) -> GpuLightSource { + Self { + kind: triangle_count << 1, + id: instance_id, + } + } + + fn new_directional_light(directional_light_id: u32) -> GpuLightSource { + Self { + kind: 1, + id: directional_light_id, + } + } +} + +#[derive(ShaderType, Default)] +struct GpuDirectionalLight { + direction_to_light: Vec3, + cos_theta_max: f32, + luminance: Vec3, + inverse_pdf: f32, +} + +impl GpuDirectionalLight { + fn new(directional_light: &ExtractedDirectionalLight) -> Self { + let cos_theta_max = cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0); + let solid_angle = TAU * (1.0 - cos_theta_max); + let luminance = + (directional_light.color.to_vec3() * directional_light.illuminance) / solid_angle; + + Self { + direction_to_light: directional_light.transform.back().into(), + cos_theta_max, + luminance, + inverse_pdf: solid_angle, + } + } +} + +fn tlas_transform(transform: &Mat4) -> [f32; 12] { + transform.transpose().to_cols_array()[..12] + .try_into() + .unwrap() +} diff --git a/crates/bevy_solari/src/scene/blas.rs b/crates/bevy_solari/src/scene/blas.rs new file mode 100644 index 0000000000..5beaa3b57c --- /dev/null +++ b/crates/bevy_solari/src/scene/blas.rs @@ -0,0 +1,133 @@ +use bevy_asset::AssetId; +use bevy_ecs::{ + resource::Resource, + system::{Res, ResMut}, +}; +use bevy_mesh::{Indices, Mesh}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{ + allocator::{MeshAllocator, MeshBufferSlice}, + RenderMesh, + }, + render_asset::ExtractedAssets, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, +}; + +#[derive(Resource, Default)] +pub struct BlasManager(HashMap, Blas>); + +impl BlasManager { + pub fn get(&self, mesh: &AssetId) -> Option<&Blas> { + self.0.get(mesh) + } +} + +pub fn prepare_raytracing_blas( + mut blas_manager: ResMut, + extracted_meshes: Res>, + mesh_allocator: Res, + render_device: Res, + render_queue: Res, +) { + let blas_manager = &mut blas_manager.0; + + // Delete BLAS for deleted or modified meshes + for asset_id in extracted_meshes + .removed + .iter() + .chain(extracted_meshes.modified.iter()) + { + blas_manager.remove(asset_id); + } + + if extracted_meshes.extracted.is_empty() { + return; + } + + // Create new BLAS for added or changed meshes + let blas_resources = extracted_meshes + .extracted + .iter() + .filter(|(_, mesh)| is_mesh_raytracing_compatible(mesh)) + .map(|(asset_id, _)| { + let vertex_slice = mesh_allocator.mesh_vertex_slice(asset_id).unwrap(); + let index_slice = mesh_allocator.mesh_index_slice(asset_id).unwrap(); + + let (blas, blas_size) = + allocate_blas(&vertex_slice, &index_slice, asset_id, &render_device); + + blas_manager.insert(*asset_id, blas); + + (*asset_id, vertex_slice, index_slice, blas_size) + }) + .collect::>(); + + // Build geometry into each BLAS + let build_entries = blas_resources + .iter() + .map(|(asset_id, vertex_slice, index_slice, blas_size)| { + let geometry = BlasTriangleGeometry { + size: blas_size, + vertex_buffer: vertex_slice.buffer, + first_vertex: vertex_slice.range.start, + vertex_stride: 48, + index_buffer: Some(index_slice.buffer), + first_index: Some(index_slice.range.start), + transform_buffer: None, + transform_buffer_offset: None, + }; + BlasBuildEntry { + blas: &blas_manager[asset_id], + geometry: BlasGeometries::TriangleGeometries(vec![geometry]), + } + }) + .collect::>(); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_blas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&build_entries, &[]); + render_queue.submit([command_encoder.finish()]); +} + +fn allocate_blas( + vertex_slice: &MeshBufferSlice, + index_slice: &MeshBufferSlice, + asset_id: &AssetId, + render_device: &RenderDevice, +) -> (Blas, BlasTriangleGeometrySizeDescriptor) { + let blas_size = BlasTriangleGeometrySizeDescriptor { + vertex_format: Mesh::ATTRIBUTE_POSITION.format, + vertex_count: vertex_slice.range.len() as u32, + index_format: Some(IndexFormat::Uint32), + index_count: Some(index_slice.range.len() as u32), + flags: AccelerationStructureGeometryFlags::OPAQUE, + }; + + let blas = render_device.wgpu_device().create_blas( + &CreateBlasDescriptor { + label: Some(&asset_id.to_string()), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + }, + BlasGeometrySizeDescriptors::Triangles { + descriptors: vec![blas_size.clone()], + }, + ); + + (blas, blas_size) +} + +fn is_mesh_raytracing_compatible(mesh: &Mesh) -> bool { + let triangle_list = mesh.primitive_topology() == PrimitiveTopology::TriangleList; + let vertex_attributes = mesh.attributes().map(|(attribute, _)| attribute.id).eq([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ]); + let indexed_32 = matches!(mesh.indices(), Some(Indices::U32(..))); + mesh.enable_raytracing && triangle_list && vertex_attributes && indexed_32 +} diff --git a/crates/bevy_solari/src/scene/extract.rs b/crates/bevy_solari/src/scene/extract.rs new file mode 100644 index 0000000000..46b11ba2b4 --- /dev/null +++ b/crates/bevy_solari/src/scene/extract.rs @@ -0,0 +1,45 @@ +use super::RaytracingMesh3d; +use bevy_asset::{AssetId, Assets}; +use bevy_derive::Deref; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Query}, +}; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_platform::collections::HashMap; +use bevy_render::{extract_resource::ExtractResource, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_raytracing_scene( + instances: Extract< + Query<( + RenderEntity, + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + >, + mut commands: Commands, +) { + for (render_entity, mesh, material, transform) in &instances { + commands + .entity(render_entity) + .insert((mesh.clone(), material.clone(), *transform)); + } +} + +#[derive(Resource, Deref, Default)] +pub struct StandardMaterialAssets(HashMap, StandardMaterial>); + +impl ExtractResource for StandardMaterialAssets { + type Source = Assets; + + fn extract_resource(source: &Self::Source) -> Self { + Self( + source + .iter() + .map(|(asset_id, material)| (asset_id, material.clone())) + .collect(), + ) + } +} diff --git a/crates/bevy_solari/src/scene/mod.rs b/crates/bevy_solari/src/scene/mod.rs new file mode 100644 index 0000000000..a68e126480 --- /dev/null +++ b/crates/bevy_solari/src/scene/mod.rs @@ -0,0 +1,77 @@ +mod binder; +mod blas; +mod extract; +mod types; + +pub use binder::RaytracingSceneBindings; +pub use types::RaytracingMesh3d; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_render::{ + extract_resource::ExtractResourcePlugin, + load_shader_library, + mesh::{ + allocator::{allocate_and_free_meshes, MeshAllocator}, + RenderMesh, + }, + render_asset::prepare_assets, + render_resource::BufferUsages, + renderer::RenderDevice, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use binder::prepare_raytracing_scene_bindings; +use blas::{prepare_raytracing_blas, BlasManager}; +use extract::{extract_raytracing_scene, StandardMaterialAssets}; +use tracing::warn; + +/// Creates acceleration structures and binding arrays of resources for raytracing. +pub struct RaytracingScenePlugin; + +impl Plugin for RaytracingScenePlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "raytracing_scene_bindings.wgsl"); + load_shader_library!(app, "sampling.wgsl"); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "RaytracingScenePlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + app.add_plugins(ExtractResourcePlugin::::default()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app + .world_mut() + .resource_mut::() + .extra_buffer_usages |= BufferUsages::BLAS_INPUT | BufferUsages::STORAGE; + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, extract_raytracing_scene) + .add_systems( + Render, + ( + prepare_raytracing_blas + .in_set(RenderSystems::PrepareAssets) + .before(prepare_assets::) + .after(allocate_and_free_meshes), + prepare_raytracing_scene_bindings.in_set(RenderSystems::PrepareBindGroups), + ), + ); + } +} diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl new file mode 100644 index 0000000000..aad064590f --- /dev/null +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -0,0 +1,164 @@ +#define_import_path bevy_solari::scene_bindings + +struct InstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +struct VertexBuffer { vertices: array } + +struct IndexBuffer { indices: array } + +struct PackedVertex { + a: vec4, + b: vec4, + tangent: vec4, +} + +struct Vertex { + position: vec3, + normal: vec3, + uv: vec2, + tangent: vec4, +} + +fn unpack_vertex(packed: PackedVertex) -> Vertex { + var vertex: Vertex; + vertex.position = packed.a.xyz; + vertex.normal = vec3(packed.a.w, packed.b.xy); + vertex.uv = packed.b.zw; + vertex.tangent = packed.tangent; + return vertex; +} + +struct Material { + base_color: vec4, + emissive: vec4, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +const TEXTURE_MAP_NONE = 0xFFFFFFFFu; + +struct LightSource { + kind: u32, // 1 bit for kind, 31 bits for extra data + id: u32, +} + +const LIGHT_SOURCE_KIND_EMISSIVE_MESH = 0u; +const LIGHT_SOURCE_KIND_DIRECTIONAL = 1u; + +struct DirectionalLight { + direction_to_light: vec3, + cos_theta_max: f32, + luminance: vec3, + inverse_pdf: f32, +} + +@group(0) @binding(0) var vertex_buffers: binding_array; +@group(0) @binding(1) var index_buffers: binding_array; +@group(0) @binding(2) var textures: binding_array>; +@group(0) @binding(3) var samplers: binding_array; +@group(0) @binding(4) var materials: array; +@group(0) @binding(5) var tlas: acceleration_structure; +@group(0) @binding(6) var transforms: array>; +@group(0) @binding(7) var geometry_ids: array; +@group(0) @binding(8) var material_ids: array; // TODO: Store material_id in instance_custom_index instead? +@group(0) @binding(9) var light_sources: array; +@group(0) @binding(10) var directional_lights: array; + +const RAY_T_MIN = 0.01; +const RAY_T_MAX = 100000.0; + +const RAY_NO_CULL = 0xFFu; + +fn trace_ray(ray_origin: vec3, ray_direction: vec3, ray_t_min: f32, ray_t_max: f32, ray_flag: u32) -> RayIntersection { + let ray = RayDesc(ray_flag, RAY_NO_CULL, ray_t_min, ray_t_max, ray_origin, ray_direction); + var rq: ray_query; + rayQueryInitialize(&rq, tlas, ray); + rayQueryProceed(&rq); + return rayQueryGetCommittedIntersection(&rq); +} + +fn sample_texture(id: u32, uv: vec2) -> vec3 { + return textureSampleLevel(textures[id], samplers[id], uv, 0.0).rgb; // TODO: Mipmap +} + +struct ResolvedMaterial { + base_color: vec3, + emissive: vec3, +} + +struct ResolvedRayHitFull { + world_position: vec3, + world_normal: vec3, + geometric_world_normal: vec3, + uv: vec2, + triangle_area: f32, + material: ResolvedMaterial, +} + +fn resolve_material(material: Material, uv: vec2) -> ResolvedMaterial { + var m: ResolvedMaterial; + + m.base_color = material.base_color.rgb; + if material.base_color_texture_id != TEXTURE_MAP_NONE { + m.base_color *= sample_texture(material.base_color_texture_id, uv); + } + + m.emissive = material.emissive.rgb; + if material.emissive_texture_id != TEXTURE_MAP_NONE { + m.emissive *= sample_texture(material.emissive_texture_id, uv); + } + + return m; +} + +fn resolve_ray_hit_full(ray_hit: RayIntersection) -> ResolvedRayHitFull { + let barycentrics = vec3(1.0 - ray_hit.barycentrics.x - ray_hit.barycentrics.y, ray_hit.barycentrics); + return resolve_triangle_data_full(ray_hit.instance_id, ray_hit.primitive_index, barycentrics); +} + +fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics: vec3) -> ResolvedRayHitFull { + let instance_geometry_ids = geometry_ids[instance_id]; + let material_id = material_ids[instance_id]; + + let index_buffer = &index_buffers[instance_geometry_ids.index_buffer_id].indices; + let vertex_buffer = &vertex_buffers[instance_geometry_ids.vertex_buffer_id].vertices; + let material = materials[material_id]; + + let indices_i = (triangle_id * 3u) + vec3(0u, 1u, 2u) + instance_geometry_ids.index_buffer_offset; + let indices = vec3((*index_buffer)[indices_i.x], (*index_buffer)[indices_i.y], (*index_buffer)[indices_i.z]) + instance_geometry_ids.vertex_buffer_offset; + let vertices = array(unpack_vertex((*vertex_buffer)[indices.x]), unpack_vertex((*vertex_buffer)[indices.y]), unpack_vertex((*vertex_buffer)[indices.z])); + + let transform = transforms[instance_id]; + let local_position = mat3x3(vertices[0].position, vertices[1].position, vertices[2].position) * barycentrics; + let world_position = (transform * vec4(local_position, 1.0)).xyz; + + let uv = mat3x2(vertices[0].uv, vertices[1].uv, vertices[2].uv) * barycentrics; + + let local_normal = mat3x3(vertices[0].normal, vertices[1].normal, vertices[2].normal) * barycentrics; // TODO: Use barycentric lerp, ray_hit.object_to_world, cross product geo normal + var world_normal = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_normal); + let geometric_world_normal = world_normal; + if material.normal_map_texture_id != TEXTURE_MAP_NONE { + let local_tangent = mat3x3(vertices[0].tangent.xyz, vertices[1].tangent.xyz, vertices[2].tangent.xyz) * barycentrics; + let world_tangent = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_tangent); + let N = world_normal; + let T = world_tangent; + let B = vertices[0].tangent.w * cross(N, T); + let Nt = sample_texture(material.normal_map_texture_id, uv); + world_normal = normalize(Nt.x * T + Nt.y * B + Nt.z * N); + } + + let triangle_edge0 = vertices[0].position - vertices[1].position; + let triangle_edge1 = vertices[0].position - vertices[2].position; + let triangle_area = length(cross(triangle_edge0, triangle_edge1)) / 2.0; + + let resolved_material = resolve_material(material, uv); + + return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, uv, triangle_area, resolved_material); +} diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl new file mode 100644 index 0000000000..06142192b6 --- /dev/null +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -0,0 +1,177 @@ +#define_import_path bevy_solari::sampling + +#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} +#import bevy_render::maths::PI_2 +#import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303 +fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = 1.0 - 2.0 * rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = normal.x + sin_theta * cos(phi); + let y = normal.y + sin_theta * sin(phi); + let z = normal.z + cos_theta; + return vec3(x, y, z); +} + +fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> vec3 { + let light_sample = generate_random_light_sample(rng); + let light_contribution = calculate_light_contribution(light_sample, ray_origin, origin_world_normal); + let visibility = trace_light_visibility(light_sample, ray_origin); + return light_contribution.radiance * visibility * light_contribution.inverse_pdf; +} + +struct LightSample { + light_id: vec2, + random: vec2, +} + +struct LightContribution { + radiance: vec3, + inverse_pdf: f32, +} + +fn generate_random_light_sample(rng: ptr) -> LightSample { + let light_count = arrayLength(&light_sources); + let light_id = rand_range_u(light_count, rng); + let random = rand_vec2f(rng); + + let light_source = light_sources[light_id]; + var triangle_id = 0u; + + if light_source.kind != LIGHT_SOURCE_KIND_DIRECTIONAL { + let triangle_count = light_source.kind >> 1u; + triangle_id = rand_range_u(triangle_count, rng); + } + + return LightSample(vec2(light_id, triangle_id), random); +} + +fn calculate_light_contribution(light_sample: LightSample, ray_origin: vec3, origin_world_normal: vec3) -> LightContribution { + let light_id = light_sample.light_id.x; + let light_source = light_sources[light_id]; + + var light_contribution: LightContribution; + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + light_contribution = calculate_directional_light_contribution(light_sample, light_source.id, origin_world_normal); + } else { + let triangle_count = light_source.kind >> 1u; + light_contribution = calculate_emissive_mesh_contribution(light_sample, light_source.id, triangle_count, ray_origin, origin_world_normal); + } + + let light_count = arrayLength(&light_sources); + light_contribution.inverse_pdf *= f32(light_count); + + return light_contribution; +} + +fn calculate_directional_light_contribution(light_sample: LightSample, directional_light_id: u32, origin_world_normal: vec3) -> LightContribution { + let directional_light = directional_lights[directional_light_id]; + +#ifdef DIRECTIONAL_LIGHT_SOFT_SHADOWS + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = light_sample.random.y * PI_2; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; +#else + let ray_direction = directional_light.direction_to_light; +#endif + + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let radiance = directional_light.luminance * cos_theta_origin; + + return LightContribution(radiance, directional_light.inverse_pdf); +} + +fn calculate_emissive_mesh_contribution(light_sample: LightSample, instance_id: u32, triangle_count: u32, ray_origin: vec3, origin_world_normal: vec3) -> LightContribution { + let barycentrics = triangle_barycentrics(light_sample.random); + let triangle_id = light_sample.light_id.y; + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let cos_theta_light = saturate(dot(-ray_direction, triangle_data.world_normal)); + let light_distance_squared = light_distance * light_distance; + + let radiance = triangle_data.material.emissive.rgb * cos_theta_origin * (cos_theta_light / light_distance_squared); + let inverse_pdf = f32(triangle_count) * triangle_data.triangle_area; + + return LightContribution(radiance, inverse_pdf); +} + +fn trace_light_visibility(light_sample: LightSample, ray_origin: vec3) -> f32 { + let light_id = light_sample.light_id.x; + let light_source = light_sources[light_id]; + + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + return trace_directional_light_visibility(light_sample, light_source.id, ray_origin); + } else { + return trace_emissive_mesh_visibility(light_sample, light_source.id, ray_origin); + } +} + +fn trace_directional_light_visibility(light_sample: LightSample, directional_light_id: u32, ray_origin: vec3) -> f32 { + let directional_light = directional_lights[directional_light_id]; + +#ifdef DIRECTIONAL_LIGHT_SOFT_SHADOWS + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = light_sample.random.y * PI_2; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; +#else + let ray_direction = directional_light.direction_to_light; +#endif + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); +} + +fn trace_emissive_mesh_visibility(light_sample: LightSample, instance_id: u32, ray_origin: vec3) -> f32 { + let barycentrics = triangle_barycentrics(light_sample.random); + let triangle_id = light_sample.light_id.y; + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + + let ray_t_max = light_distance - RAY_T_MIN - RAY_T_MIN; + if ray_t_max < RAY_T_MIN { return 0.0; } + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, ray_t_max, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec22%3A297 +fn triangle_barycentrics(random: vec2) -> vec3 { + var barycentrics = random; + if barycentrics.x + barycentrics.y > 1.0 { barycentrics = 1.0 - barycentrics; } + return vec3(1.0 - barycentrics.x - barycentrics.y, barycentrics); +} + +// https://jcgt.org/published/0006/01/01/paper.pdf +fn build_orthonormal_basis(normal: vec3) -> mat3x3 { + let sign = select(-1.0, 1.0, normal.z >= 0.0); + let a = -1.0 / (sign + normal.z); + let b = normal.x * normal.y * a; + let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x); + let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y); + return mat3x3(tangent, bitangent, normal); +} diff --git a/crates/bevy_solari/src/scene/types.rs b/crates/bevy_solari/src/scene/types.rs new file mode 100644 index 0000000000..8ee33b31fc --- /dev/null +++ b/crates/bevy_solari/src/scene/types.rs @@ -0,0 +1,21 @@ +use bevy_asset::Handle; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, prelude::ReflectComponent}; +use bevy_mesh::Mesh; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::sync_world::SyncToRenderWorld; +use bevy_transform::components::Transform; +use derive_more::derive::From; + +/// A mesh component used for raytracing. +/// +/// The mesh used in this component must have [`bevy_render::mesh::Mesh::enable_raytracing`] set to true, +/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_render::render_resource::PrimitiveTopology::TriangleList`], +/// and use [`bevy_mesh::Indices::U32`]. +/// +/// The material used for this entity must be [`MeshMaterial3d`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[reflect(Component, Default, Clone, PartialEq)] +#[require(MeshMaterial3d, Transform, SyncToRenderWorld)] +pub struct RaytracingMesh3d(pub Handle); diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 8fa5bae2cc..1d356fdc40 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_sprite" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides sprite functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -15,21 +15,21 @@ webgpu = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev", optional = true } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev", optional = true } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 4b2d206420..882ec5857c 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -2,8 +2,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! Provides 2D sprite rendering functionality. @@ -42,16 +42,17 @@ pub use sprite::*; pub use texture_slice::*; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetEventSystems, Assets, Handle}; +use bevy_asset::{embedded_asset, AssetEventSystems, Assets}; use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; use bevy_ecs::prelude::*; use bevy_image::{prelude::*, TextureAtlasPlugin}; use bevy_render::{ batching::sort_binned_render_phase, + load_shader_library, mesh::{Mesh, Mesh2d, MeshAabb}, primitives::Aabb, render_phase::AddRenderCommand, - render_resource::{Shader, SpecializedRenderPipelines}, + render_resource::SpecializedRenderPipelines, view::{NoFrustumCulling, VisibilitySystems}, ExtractSchedule, Render, RenderApp, RenderSystems, }; @@ -60,11 +61,6 @@ use bevy_render::{ #[derive(Default)] pub struct SpritePlugin; -pub const SPRITE_SHADER_HANDLE: Handle = - weak_handle!("ed996613-54c0-49bd-81be-1c2d1a0d03c2"); -pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle = - weak_handle!("43947210-8df6-459a-8f2a-12f350d174cc"); - /// System set for sprite rendering. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum SpriteSystems { @@ -78,18 +74,9 @@ pub type SpriteSystem = SpriteSystems; impl Plugin for SpritePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - SPRITE_SHADER_HANDLE, - "render/sprite.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - SPRITE_VIEW_BINDINGS_SHADER_HANDLE, - "render/sprite_view_bindings.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "render/sprite_view_bindings.wgsl"); + + embedded_asset!(app, "render/sprite.wgsl"); if !app.is_plugin_added::() { app.add_plugins(TextureAtlasPlugin); @@ -169,9 +156,9 @@ pub fn calculate_bounds_2d( atlases: Res>, meshes_without_aabb: Query<(Entity, &Mesh2d), (Without, Without)>, sprites_to_recalculate_aabb: Query< - (Entity, &Sprite), + (Entity, &Sprite, &Anchor), ( - Or<(Without, Changed)>, + Or<(Without, Changed, Changed)>, Without, ), >, @@ -183,7 +170,7 @@ pub fn calculate_bounds_2d( } } } - for (entity, sprite) in &sprites_to_recalculate_aabb { + for (entity, sprite, anchor) in &sprites_to_recalculate_aabb { if let Some(size) = sprite .custom_size .or_else(|| sprite.rect.map(|rect| rect.size())) @@ -197,7 +184,7 @@ pub fn calculate_bounds_2d( }) { let aabb = Aabb { - center: (-sprite.anchor.as_vec() * size).extend(0.0).into(), + center: (-anchor.as_vec() * size).extend(0.0).into(), half_extents: (0.5 * size).extend(0.0).into(), }; commands.entity(entity).try_insert(aabb); @@ -334,12 +321,14 @@ mod test { // Add entities let entity = app .world_mut() - .spawn(Sprite { - rect: Some(Rect::new(0., 0., 0.5, 1.)), - anchor: Anchor::TOP_RIGHT, - image: image_handle, - ..default() - }) + .spawn(( + Sprite { + rect: Some(Rect::new(0., 0., 0.5, 1.)), + image: image_handle, + ..default() + }, + Anchor::TOP_RIGHT, + )) .id(); // Create AABB diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 83b6930776..d814cfc384 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,26 +1,18 @@ use crate::{AlphaMode2d, Material2d, Material2dPlugin}; use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, weak_handle, Asset, AssetApp, Assets, Handle}; +use bevy_asset::{embedded_asset, embedded_path, Asset, AssetApp, AssetPath, Assets, Handle}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; use bevy_image::Image; use bevy_math::{Affine2, Mat3, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{render_asset::RenderAssets, render_resource::*, texture::GpuImage}; -pub const COLOR_MATERIAL_SHADER_HANDLE: Handle = - weak_handle!("92e0e6e9-ed0b-4db3-89ab-5f65d3678250"); - #[derive(Default)] pub struct ColorMaterialPlugin; impl Plugin for ColorMaterialPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - COLOR_MATERIAL_SHADER_HANDLE, - "color_material.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "color_material.wgsl"); app.add_plugins(Material2dPlugin::::default()) .register_asset_reflect::(); @@ -152,7 +144,9 @@ impl AsBindGroupShaderType for ColorMaterial { impl Material2d for ColorMaterial { fn fragment_shader() -> ShaderRef { - COLOR_MATERIAL_SHADER_HANDLE.into() + ShaderRef::Path( + AssetPath::from_path_buf(embedded_path!("color_material.wgsl")).with_source("embedded"), + ) } fn alpha_mode(&self) -> AlphaMode2d { diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 3bacc35194..204dd85e24 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,5 +1,6 @@ use bevy_app::Plugin; -use bevy_asset::{load_internal_asset, weak_handle, AssetId, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId, Handle}; +use bevy_render::load_shader_library; use crate::{tonemapping_pipeline_key, Material2dBindGroupId}; use bevy_core_pipeline::tonemapping::DebandDither; @@ -57,54 +58,15 @@ use tracing::error; #[derive(Default)] pub struct Mesh2dRenderPlugin; -pub const MESH2D_VERTEX_OUTPUT: Handle = - weak_handle!("71e279c7-85a0-46ac-9a76-1586cbf506d0"); -pub const MESH2D_VIEW_TYPES_HANDLE: Handle = - weak_handle!("01087b0d-91e9-46ac-8628-dfe19a7d4b83"); -pub const MESH2D_VIEW_BINDINGS_HANDLE: Handle = - weak_handle!("fbdd8b80-503d-4688-bcec-db29ab4620b2"); -pub const MESH2D_TYPES_HANDLE: Handle = - weak_handle!("199f2089-6e99-4348-9bb1-d82816640a7f"); -pub const MESH2D_BINDINGS_HANDLE: Handle = - weak_handle!("a7bd44cc-0580-4427-9a00-721cf386b6e4"); -pub const MESH2D_FUNCTIONS_HANDLE: Handle = - weak_handle!("0d08ff71-68c1-4017-83e2-bfc34d285c51"); -pub const MESH2D_SHADER_HANDLE: Handle = - weak_handle!("91a7602b-df95-4ea3-9d97-076abcb69d91"); - impl Plugin for Mesh2dRenderPlugin { fn build(&self, app: &mut bevy_app::App) { - load_internal_asset!( - app, - MESH2D_VERTEX_OUTPUT, - "mesh2d_vertex_output.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_VIEW_TYPES_HANDLE, - "mesh2d_view_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_VIEW_BINDINGS_HANDLE, - "mesh2d_view_bindings.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_TYPES_HANDLE, - "mesh2d_types.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - MESH2D_FUNCTIONS_HANDLE, - "mesh2d_functions.wgsl", - Shader::from_wgsl - ); - load_internal_asset!(app, MESH2D_SHADER_HANDLE, "mesh2d.wgsl", Shader::from_wgsl); + load_shader_library!(app, "mesh2d_vertex_output.wgsl"); + load_shader_library!(app, "mesh2d_view_types.wgsl"); + load_shader_library!(app, "mesh2d_view_bindings.wgsl"); + load_shader_library!(app, "mesh2d_types.wgsl"); + load_shader_library!(app, "mesh2d_functions.wgsl"); + + embedded_asset!(app, "mesh2d.wgsl"); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -168,13 +130,10 @@ impl Plugin for Mesh2dRenderPlugin { // Load the mesh_bindings shader module here as it depends on runtime information about // whether storage buffers are supported, or the maximum uniform buffer binding size. - load_internal_asset!( - app, - MESH2D_BINDINGS_HANDLE, - "mesh2d_bindings.wgsl", - Shader::from_wgsl_with_defs, - mesh_bindings_shader_defs - ); + load_shader_library!(app, "mesh2d_bindings.wgsl", move |settings| *settings = + ShaderSettings { + shader_defs: mesh_bindings_shader_defs.clone() + }); } } @@ -316,6 +275,7 @@ pub fn extract_mesh2d( pub struct Mesh2dPipeline { pub view_layout: BindGroupLayout, pub mesh_layout: BindGroupLayout, + pub shader: Handle, // This dummy white texture is to be used in place of optional textures pub dummy_white_gpu_image: GpuImage, pub per_object_buffer_batch_size: Option, @@ -397,6 +357,7 @@ impl FromWorld for Mesh2dPipeline { per_object_buffer_batch_size: GpuArrayBuffer::::batch_size( render_device, ), + shader: load_embedded_asset!(world, "mesh2d.wgsl"), } } } @@ -690,13 +651,13 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { Ok(RenderPipelineDescriptor { vertex: VertexState { - shader: MESH2D_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], }, fragment: Some(FragmentState { - shader: MESH2D_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -817,7 +778,7 @@ impl RenderCommand

for SetMesh2dViewBindGroup( _item: &P, - (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _view: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 468a47f6bb..03de94be1c 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -4,7 +4,7 @@ use crate::{ }; use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ - load_internal_asset, prelude::AssetChanged, weak_handle, AsAssetId, Asset, AssetApp, + embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; @@ -54,9 +54,6 @@ use bevy_render::{ use core::{hash::Hash, ops::Range}; use tracing::error; -pub const WIREFRAME_2D_SHADER_HANDLE: Handle = - weak_handle!("2d8a3853-2927-4de2-9dc7-3971e7e40970"); - /// A [`Plugin`] that draws wireframes for 2D meshes. /// /// Wireframes currently do not work when using webgl or webgpu. @@ -81,12 +78,7 @@ impl Wireframe2dPlugin { impl Plugin for Wireframe2dPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - WIREFRAME_2D_SHADER_HANDLE, - "wireframe2d.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "wireframe2d.wgsl"); app.add_plugins(( BinnedRenderPhasePlugin::::new(self.debug_flags), @@ -339,7 +331,7 @@ impl FromWorld for Wireframe2dPipeline { fn from_world(render_world: &mut World) -> Self { Wireframe2dPipeline { mesh_pipeline: render_world.resource::().clone(), - shader: WIREFRAME_2D_SHADER_HANDLE, + shader: load_embedded_asset!(render_world, "wireframe2d.wgsl"), } } } @@ -380,7 +372,7 @@ impl ViewNode for Wireframe2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(wireframe_phase) = diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 56579c9c0a..57c1acc6bd 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -10,7 +10,7 @@ //! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized //! vector provided by the target's `GlobalTransform::back()`. -use crate::Sprite; +use crate::{Anchor, Sprite}; use bevy_app::prelude::*; use bevy_asset::prelude::*; use bevy_color::Alpha; @@ -100,6 +100,7 @@ fn sprite_picking( Entity, &Sprite, &GlobalTransform, + &Anchor, &Pickable, &ViewVisibility, )>, @@ -107,9 +108,9 @@ fn sprite_picking( ) { let mut sorted_sprites: Vec<_> = sprite_query .iter() - .filter_map(|(entity, sprite, transform, pickable, vis)| { + .filter_map(|(entity, sprite, transform, anchor, pickable, vis)| { if !transform.affine().is_nan() && vis.get() { - Some((entity, sprite, transform, pickable)) + Some((entity, sprite, transform, anchor, pickable)) } else { None } @@ -117,7 +118,7 @@ fn sprite_picking( .collect(); // radsort is a stable radix sort that performed better than `slice::sort_by_key` - radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| { + radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _, _)| { -transform.translation().z }); @@ -159,7 +160,7 @@ fn sprite_picking( let picks: Vec<(Entity, HitData)> = sorted_sprites .iter() .copied() - .filter_map(|(entity, sprite, sprite_transform, pickable)| { + .filter_map(|(entity, sprite, sprite_transform, anchor, pickable)| { if blocked { return None; } @@ -192,6 +193,7 @@ fn sprite_picking( let Ok(cursor_pixel_space) = sprite.compute_pixel_space_point( cursor_pos_sprite, + *anchor, &images, &texture_atlas_layout, ) else { diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index de57f43536..761e2c628a 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,7 +1,7 @@ use core::ops::Range; -use crate::{ComputedTextureSlices, ScalingMode, Sprite, SPRITE_SHADER_HANDLE}; -use bevy_asset::{AssetEvent, AssetId, Assets}; +use crate::{Anchor, ComputedTextureSlices, ScalingMode, Sprite}; +use bevy_asset::{load_embedded_asset, AssetEvent, AssetId, Assets, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, @@ -47,6 +47,7 @@ use fixedbitset::FixedBitSet; pub struct SpritePipeline { view_layout: BindGroupLayout, material_layout: BindGroupLayout, + shader: Handle, pub dummy_white_gpu_image: GpuImage, } @@ -124,6 +125,7 @@ impl FromWorld for SpritePipeline { view_layout, material_layout, dummy_white_gpu_image, + shader: load_embedded_asset!(world, "sprite.wgsl"), } } } @@ -267,13 +269,13 @@ impl SpecializedRenderPipeline for SpritePipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: SPRITE_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![instance_rate_vertex_buffer_layout], }, fragment: Some(FragmentState { - shader: SPRITE_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -394,13 +396,14 @@ pub fn extract_sprites( &ViewVisibility, &Sprite, &GlobalTransform, + &Anchor, Option<&ComputedTextureSlices>, )>, >, ) { extracted_sprites.sprites.clear(); extracted_slices.slices.clear(); - for (main_entity, render_entity, view_visibility, sprite, transform, slices) in + for (main_entity, render_entity, view_visibility, sprite, transform, anchor, slices) in sprite_query.iter() { if !view_visibility.get() { @@ -411,7 +414,7 @@ pub fn extract_sprites( let start = extracted_slices.slices.len(); extracted_slices .slices - .extend(slices.extract_slices(sprite)); + .extend(slices.extract_slices(sprite, anchor.as_vec())); let end = extracted_slices.slices.len(); extracted_sprites.sprites.push(ExtractedSprite { main_entity, @@ -451,7 +454,7 @@ pub fn extract_sprites( flip_y: sprite.flip_y, image_handle_id: sprite.image.id(), kind: ExtractedSpriteKind::Single { - anchor: sprite.anchor.as_vec(), + anchor: anchor.as_vec(), rect, scaling_mode: sprite.image_mode.scale(), // Pass the custom size @@ -905,7 +908,7 @@ impl RenderCommand

for SetSpriteViewBindGroup( _item: &P, - (view_uniform, sprite_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, sprite_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -922,7 +925,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (image_bind_groups, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -952,7 +955,7 @@ impl RenderCommand

for DrawSpriteBatch { fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (sprite_meta, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 32b8ebb49e..61461ab640 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -15,7 +15,7 @@ use crate::TextureSlicer; /// Describes a sprite to be rendered to a 2D camera #[derive(Component, Debug, Default, Clone, Reflect)] -#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass)] +#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass, Anchor)] #[reflect(Component, Default, Debug, Clone)] #[component(on_add = view::add_visibility_class::)] pub struct Sprite { @@ -38,8 +38,6 @@ pub struct Sprite { /// When used with a [`TextureAtlas`], the rect /// is offset by the atlas's minimal (top-left) corner position. pub rect: Option, - /// [`Anchor`] point of the sprite in the world - pub anchor: Anchor, /// How the sprite's image will be scaled. pub image_mode: SpriteImageMode, } @@ -86,6 +84,7 @@ impl Sprite { pub fn compute_pixel_space_point( &self, point_relative_to_sprite: Vec2, + anchor: Anchor, images: &Assets, texture_atlases: &Assets, ) -> Result { @@ -112,7 +111,7 @@ impl Sprite { }; let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size()); - let sprite_center = -self.anchor.as_vec() * sprite_size; + let sprite_center = -anchor.as_vec() * sprite_size; let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center; @@ -315,8 +314,14 @@ mod tests { ..Default::default() }; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) + }; assert_eq!(compute(Vec2::new(-2.0, -4.5)), Ok(Vec2::new(0.5, 9.5))); assert_eq!(compute(Vec2::new(0.0, 0.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(0.0, 4.5)), Ok(Vec2::new(2.5, 0.5))); @@ -334,7 +339,12 @@ mod tests { let compute = |point| { sprite - .compute_pixel_space_point(point, &image_assets, &texture_atlas_assets) + .compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) // Round to remove floating point errors. .map(|x| (x * 1e5).round() / 1e5) .map_err(|x| (x * 1e5).round() / 1e5) @@ -355,12 +365,13 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(0.5, 0.5))); assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5))); @@ -377,12 +388,13 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::TOP_RIGHT, ..Default::default() }; + let anchor = Anchor::TOP_RIGHT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 0.5))); assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 0.5))); @@ -399,13 +411,14 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, flip_x: true, ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 9.5)), Ok(Vec2::new(4.5, 0.5))); assert_eq!(compute(Vec2::new(2.5, 5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(2.5, 9.5)), Ok(Vec2::new(2.5, 0.5))); @@ -422,13 +435,14 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::TOP_RIGHT, flip_y: true, ..Default::default() }; + let anchor = Anchor::TOP_RIGHT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(-4.5, -0.5)), Ok(Vec2::new(0.5, 9.5))); assert_eq!(compute(Vec2::new(-2.5, -5.0)), Ok(Vec2::new(2.5, 5.0))); assert_eq!(compute(Vec2::new(-2.5, -0.5)), Ok(Vec2::new(2.5, 9.5))); @@ -446,12 +460,13 @@ mod tests { let sprite = Sprite { image, rect: Some(Rect::new(1.5, 3.0, 3.0, 9.5)), - anchor: Anchor::BOTTOM_LEFT, ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(2.0, 9.0))); // The pixel is outside the rect, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(2.0, 2.5)), Err(Vec2::new(3.5, 7.0))); @@ -470,16 +485,17 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, texture_atlas: Some(TextureAtlas { layout: texture_atlas, index: 0, }), ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(1.5, 3.5))); // The pixel is outside the texture atlas, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(5.0, 1.5))); @@ -498,7 +514,6 @@ mod tests { let sprite = Sprite { image, - anchor: Anchor::BOTTOM_LEFT, texture_atlas: Some(TextureAtlas { layout: texture_atlas, index: 0, @@ -507,9 +522,11 @@ mod tests { rect: Some(Rect::new(1.5, 1.5, 3.0, 3.0)), ..Default::default() }; + let anchor = Anchor::BOTTOM_LEFT; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) + }; assert_eq!(compute(Vec2::new(0.5, 0.5)), Ok(Vec2::new(3.0, 3.5))); // The pixel is outside the texture atlas, but is still a valid pixel in the image. assert_eq!(compute(Vec2::new(4.0, 2.5)), Err(Vec2::new(6.5, 1.5))); @@ -529,8 +546,14 @@ mod tests { ..Default::default() }; - let compute = - |point| sprite.compute_pixel_space_point(point, &image_assets, &texture_atlas_assets); + let compute = |point| { + sprite.compute_pixel_space_point( + point, + Anchor::default(), + &image_assets, + &texture_atlas_assets, + ) + }; assert_eq!(compute(Vec2::new(30.0, 15.0)), Ok(Vec2::new(4.0, 1.0))); assert_eq!(compute(Vec2::new(-10.0, -15.0)), Ok(Vec2::new(2.0, 4.0))); // The pixel is outside the texture atlas, but is still a valid pixel in the image. diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index f36cf4bfac..d4972f0384 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -1,6 +1,5 @@ -use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout}; - use super::TextureSlice; +use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout}; use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; use bevy_image::Image; @@ -23,6 +22,7 @@ impl ComputedTextureSlices { pub(crate) fn extract_slices<'a>( &'a self, sprite: &'a Sprite, + anchor: Vec2, ) -> impl ExactSizeIterator + 'a { let mut flip = Vec2::ONE; if sprite.flip_x { @@ -31,7 +31,7 @@ impl ComputedTextureSlices { if sprite.flip_y { flip.y *= -1.0; } - let anchor = sprite.anchor.as_vec() + let anchor = anchor * sprite .custom_size .unwrap_or(sprite.rect.unwrap_or_default().size()); diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml index 1ae52fa571..17f9d6c787 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_state" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Finite state machines for Bevy" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -30,7 +30,6 @@ bevy_app = ["dep:bevy_app"] ## supported platforms. std = [ "bevy_ecs/std", - "bevy_utils/std", "bevy_reflect?/std", "bevy_app?/std", "bevy_platform/std", @@ -40,7 +39,6 @@ std = [ ## on all platforms, including `no_std`. critical-section = [ "bevy_ecs/critical-section", - "bevy_utils/critical-section", "bevy_app?/critical-section", "bevy_reflect?/critical-section", "bevy_platform/critical-section", @@ -48,12 +46,12 @@ critical-section = [ [dependencies] # bevy -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_state_macros = { path = "macros", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_state_macros = { path = "macros", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } variadics_please = "1.1" # other diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml index 2f569f395e..2ab531a9b2 100644 --- a/crates/bevy_state/macros/Cargo.toml +++ b/crates/bevy_state/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_state_macros" -version = "0.16.0-dev" +version = "0.17.0-dev" description = "Macros for bevy_state" edition = "2024" license = "MIT OR Apache-2.0" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" } syn = { version = "2.0", features = ["full"] } quote = "1.0" diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs index f461f0ead2..3c5a2d0674 100644 --- a/crates/bevy_state/macros/src/lib.rs +++ b/crates/bevy_state/macros/src/lib.rs @@ -1,6 +1,7 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +//! Macros for deriving `States` and `SubStates` traits. + extern crate proc_macro; mod states; @@ -8,11 +9,15 @@ mod states; use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; +/// Implements the `States` trait for a type - see the trait +/// docs for an example usage. #[proc_macro_derive(States, attributes(states))] pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } +/// Implements the `SubStates` trait for a type - see the trait +/// docs for an example usage. #[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 52c133f6ee..57c997156b 100644 --- a/crates/bevy_state/macros/src/states.rs +++ b/crates/bevy_state/macros/src/states.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, LitBool, Pat, Path, Result}; use crate::bevy_state_path; @@ -13,14 +13,16 @@ struct StatesAttrs { fn parse_states_attr(ast: &DeriveInput) -> Result { let mut attrs = StatesAttrs { - scoped_entities_enabled: false, + scoped_entities_enabled: true, }; 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; + if let Ok(value) = nested.value() { + attrs.scoped_entities_enabled = value.parse::()?.value(); + } Ok(()) } else { Err(nested.error("Unsupported attribute")) diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 903a098137..05116cbcc5 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -25,7 +25,7 @@ pub trait AppExtStates { /// These schedules are triggered before [`Update`](bevy_app::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can - /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`Condition`](bevy_ecs::prelude::Condition). + /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`SystemCondition`](bevy_ecs::prelude::SystemCondition). /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. @@ -41,7 +41,7 @@ pub trait AppExtStates { /// These schedules are triggered before [`Update`](bevy_app::Update) and at startup. /// /// If you would like to control how other systems run based on the current state, you can - /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`Condition`](bevy_ecs::prelude::Condition). + /// emulate this behavior using the [`in_state`](crate::condition::in_state) [`SystemCondition`](bevy_ecs::prelude::SystemCondition). /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. @@ -59,10 +59,11 @@ 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. + /// This is enabled by default. If you don't want this behavior, add the `#[states(scoped_entities = false)]` + /// attribute when deriving the [`States`] trait. /// /// For more information refer to [`crate::state_scoped`]. + #[doc(hidden)] fn enable_state_scoped_entities(&mut self) -> &mut Self; #[cfg(feature = "bevy_reflect")] @@ -214,6 +215,7 @@ impl AppExtStates for SubApp { self } + #[doc(hidden)] fn enable_state_scoped_entities(&mut self) -> &mut Self { if !self .world() @@ -285,6 +287,7 @@ impl AppExtStates for App { self } + #[doc(hidden)] fn enable_state_scoped_entities(&mut self) -> &mut Self { self.main_mut().enable_state_scoped_entities::(); self diff --git a/crates/bevy_state/src/condition.rs b/crates/bevy_state/src/condition.rs index faede71be5..4d9acb8cfe 100644 --- a/crates/bevy_state/src/condition.rs +++ b/crates/bevy_state/src/condition.rs @@ -1,7 +1,7 @@ use crate::state::{State, States}; use bevy_ecs::{change_detection::DetectChanges, system::Res}; -/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// A [`SystemCondition`](bevy_ecs::prelude::SystemCondition)-satisfying system that returns `true` /// if the state machine exists. /// /// # Example @@ -48,7 +48,7 @@ pub fn state_exists(current_state: Option>>) -> bool { current_state.is_some() } -/// Generates a [`Condition`](bevy_ecs::prelude::Condition)-satisfying closure that returns `true` +/// Generates a [`SystemCondition`](bevy_ecs::prelude::SystemCondition)-satisfying closure that returns `true` /// if the state machine is currently in `state`. /// /// Will return `false` if the state does not exist or if not in `state`. @@ -107,7 +107,7 @@ pub fn in_state(state: S) -> impl FnMut(Option>>) -> boo } } -/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// A [`SystemCondition`](bevy_ecs::prelude::SystemCondition)-satisfying system that returns `true` /// if the state machine changed state. /// /// To do things on transitions to/from specific states, use their respective OnEnter/OnExit @@ -171,7 +171,7 @@ pub fn state_changed(current_state: Option>>) -> bool { #[cfg(test)] mod tests { - use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule}; + use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule, SystemCondition}; use crate::prelude::*; use bevy_state_macros::States; diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs index 9267478281..61ee0627a3 100644 --- a/crates/bevy_state/src/state/mod.rs +++ b/crates/bevy_state/src/state/mod.rs @@ -642,6 +642,203 @@ mod tests { } } + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum MultiSourceComputedState { + FromSimpleBTrue, + FromSimple2B2, + FromBoth, + } + + impl ComputedStates for MultiSourceComputedState { + type SourceStates = (SimpleState, SimpleState2); + + fn compute((simple_state, simple_state2): (SimpleState, SimpleState2)) -> Option { + match (simple_state, simple_state2) { + // If both are in their special states, prioritize the "both" variant. + (SimpleState::B(true), SimpleState2::B2) => Some(Self::FromBoth), + // If only SimpleState is B(true). + (SimpleState::B(true), _) => Some(Self::FromSimpleBTrue), + // If only SimpleState2 is B2. + (_, SimpleState2::B2) => Some(Self::FromSimple2B2), + // Otherwise, no computed state. + _ => None, + } + } + } + + /// This test ensures that [`ComputedStates`] with multiple source states + /// react when any source changes. + #[test] + fn computed_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceComputedState::register_computed_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceComputedState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The computed state should exist because SimpleState changed to + // B(true). + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + + // Reset SimpleState to A - computed state should be removed. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The computed state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimple2B2 + ); + + // Test that changes to both states work. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + } + + // Test SubState that depends on multiple source states. + #[derive(PartialEq, Eq, Debug, Default, Hash, Clone)] + enum MultiSourceSubState { + #[default] + Active, + } + + impl SubStates for MultiSourceSubState { + type SourceStates = (SimpleState, SimpleState2); + + fn should_exist( + (simple_state, simple_state2): (SimpleState, SimpleState2), + ) -> Option { + // SubState should exist when: + // - SimpleState is B(true), OR + // - SimpleState2 is B2 + match (simple_state, simple_state2) { + (SimpleState::B(true), _) | (_, SimpleState2::B2) => Some(Self::Active), + _ => None, + } + } + } + + impl States for MultiSourceSubState { + const DEPENDENCY_DEPTH: usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl FreelyMutableState for MultiSourceSubState {} + + /// This test ensures that [`SubStates`] with multiple source states react + /// when any source changes. + #[test] + fn sub_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceSubState::register_sub_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceSubState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceSubState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The sub state should exist because SimpleState changed to B(true). + assert!(world.contains_resource::>()); + + // Reset to initial state. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceSubState creation. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The sub state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + + // Finally, test that it works when both change simultaneously. + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + // After this transition, the state should not exist since SimpleState + // is B(false). + assert!(!world.contains_resource::>()); + + // Change both at the same time. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert!(world.contains_resource::>()); + } + #[test] fn check_transition_orders() { let mut world = World::new(); diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index 69a6c41b3d..3cf1e1d260 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -293,7 +293,7 @@ macro_rules! impl_state_set_sealed_tuples { current_state_res: Option>>, next_state_res: Option>>, ($($val),*,): ($(Option>>),*,)| { - let parent_changed = ($($evt.read().last().is_some())&&*); + let parent_changed = ($($evt.read().last().is_some())||*); let next_state = take_next_state(next_state_res); if !parent_changed && next_state.is_none() { diff --git a/crates/bevy_state/src/state/sub_states.rs b/crates/bevy_state/src/state/sub_states.rs index 745c4baf0b..c6844eed28 100644 --- a/crates/bevy_state/src/state/sub_states.rs +++ b/crates/bevy_state/src/state/sub_states.rs @@ -7,7 +7,7 @@ pub use bevy_state_macros::SubStates; /// but unlike [`ComputedStates`](crate::state::ComputedStates) - while they exist they can be manually modified. /// /// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state -/// and value to determine it's existence. +/// and value to determine its existence. /// /// ``` /// # use bevy_ecs::prelude::*; diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index dfe711f245..1ee21826c3 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -1,7 +1,7 @@ use core::{marker::PhantomData, mem}; use bevy_ecs::{ - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, @@ -55,11 +55,11 @@ pub struct OnTransition { #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] pub struct StateTransition; -/// Event sent when any state transition of `S` happens. +/// A [`BufferedEvent`] sent when any state transition of `S` happens. /// This includes identity transitions, where `exited` and `entered` have the same value. /// /// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event, BufferedEvent)] pub struct StateTransitionEvent { /// The state being exited. pub exited: Option, diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index c591d0c108..1abf0975ea 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -14,8 +14,7 @@ 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 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 you need to disable this behavior, add the attribute `#[states(scoped_entities = false)]` when deriving [`States`]. /// /// ``` /// use bevy_state::prelude::*; @@ -23,7 +22,6 @@ use crate::state::{StateTransitionEvent, States}; /// use bevy_ecs::system::ScheduleSystem; /// /// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// #[states(scoped_entities)] /// enum GameState { /// #[default] /// MainMenu, @@ -44,7 +42,6 @@ use crate::state::{StateTransitionEvent, States}; /// # struct AppMock; /// # impl AppMock { /// # fn init_state(&mut self) {} -/// # fn enable_state_scoped_entities(&mut self) {} /// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} /// # } /// # struct Update; @@ -123,14 +120,12 @@ pub fn despawn_entities_on_exit_state( /// # struct AppMock; /// # impl AppMock { /// # fn init_state(&mut self) {} -/// # fn enable_state_scoped_entities(&mut self) {} /// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} /// # } /// # struct Update; /// # 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_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index 9defa4d888..adba1ca6b6 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -3,36 +3,50 @@ use core::marker::PhantomData; use bevy_app::{App, SubApp}; use bevy_ecs::{ - event::{Event, EventReader, Events}, + event::{BufferedEvent, EventReader, Events}, resource::Resource, system::Commands, world::World, }; use bevy_platform::collections::HashMap; -use crate::state::{FreelyMutableState, OnExit, StateTransitionEvent}; +use crate::state::{OnEnter, OnExit, StateTransitionEvent, States}; -fn clear_event_queue(w: &mut World) { +fn clear_event_queue(w: &mut World) { if let Some(mut queue) = w.get_resource_mut::>() { queue.clear(); } } -#[derive(Resource)] -struct StateScopedEvents { - cleanup_fns: HashMap>, +#[derive(Copy, Clone)] +enum TransitionType { + OnExit, + OnEnter, } -impl StateScopedEvents { - fn add_event(&mut self, state: S) { - self.cleanup_fns - .entry(state) - .or_default() - .push(clear_event_queue::); +#[derive(Resource)] +struct StateScopedEvents { + /// Keeps track of which events need to be reset when the state is exited. + on_exit: HashMap>, + /// Keeps track of which events need to be reset when the state is entered. + on_enter: HashMap>, +} + +impl StateScopedEvents { + fn add_event(&mut self, state: S, transition_type: TransitionType) { + let map = match transition_type { + TransitionType::OnExit => &mut self.on_exit, + TransitionType::OnEnter => &mut self.on_enter, + }; + map.entry(state).or_default().push(clear_event_queue::); } - fn cleanup(&self, w: &mut World, state: S) { - let Some(fns) = self.cleanup_fns.get(&state) else { + fn cleanup(&self, w: &mut World, state: S, transition_type: TransitionType) { + let map = match transition_type { + TransitionType::OnExit => &self.on_exit, + TransitionType::OnEnter => &self.on_enter, + }; + let Some(fns) = map.get(&state) else { return; }; for callback in fns { @@ -41,15 +55,16 @@ impl StateScopedEvents { } } -impl Default for StateScopedEvents { +impl Default for StateScopedEvents { fn default() -> Self { Self { - cleanup_fns: HashMap::default(), + on_exit: HashMap::default(), + on_enter: HashMap::default(), } } } -fn cleanup_state_scoped_event( +fn clear_events_on_exit_state( mut c: Commands, mut transitions: EventReader>, ) { @@ -65,48 +80,186 @@ fn cleanup_state_scoped_event( c.queue(move |w: &mut World| { w.resource_scope::, ()>(|w, events| { - events.cleanup(w, exited); + events.cleanup(w, exited, TransitionType::OnExit); }); }); } -fn add_state_scoped_event_impl( +fn clear_events_on_enter_state( + mut c: Commands, + mut transitions: EventReader>, +) { + let Some(transition) = transitions.read().last() else { + return; + }; + if transition.entered == transition.exited { + return; + } + let Some(entered) = transition.entered.clone() else { + return; + }; + + c.queue(move |w: &mut World| { + w.resource_scope::, ()>(|w, events| { + events.cleanup(w, entered, TransitionType::OnEnter); + }); + }); +} + +fn clear_events_on_state_transition( app: &mut SubApp, _p: PhantomData, state: S, + transition_type: TransitionType, ) { if !app.world().contains_resource::>() { app.init_resource::>(); } - app.add_event::(); app.world_mut() .resource_mut::>() - .add_event::(state.clone()); - app.add_systems(OnExit(state), cleanup_state_scoped_event::); + .add_event::(state.clone(), transition_type); + match transition_type { + TransitionType::OnExit => app.add_systems(OnExit(state), clear_events_on_exit_state::), + TransitionType::OnEnter => { + app.add_systems(OnEnter(state), clear_events_on_enter_state::) + } + }; } /// Extension trait for [`App`] adding methods for registering state scoped events. pub trait StateScopedEventsAppExt { - /// Adds an [`Event`] that is automatically cleaned up when leaving the specified `state`. + /// Clears an [`BufferedEvent`] when exiting the specified `state`. /// - /// Note that event cleanup is ordered ambiguously relative to [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) - /// and [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity - /// cleanup and the [`OnExit`] schedule for the target state. All of these (state scoped - /// entities and events cleanup, and `OnExit`) occur within schedule [`StateTransition`](crate::prelude::StateTransition) + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, + /// and the [`OnExit`] schedule for the target state. + /// All of these (state scoped entities and events cleanup, and `OnExit`) + /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self; + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + + /// Clears an [`BufferedEvent`] when entering the specified `state`. + /// + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, + /// and the [`OnEnter`] schedule for the target state. + /// All of these (state scoped entities and events cleanup, and `OnEnter`) + /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) + /// and system set `StateTransitionSystems::EnterSchedules`. + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; } impl StateScopedEventsAppExt for App { - fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self { - add_state_scoped_event_impl(self.main_mut(), PhantomData::, state); + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition( + self.main_mut(), + PhantomData::, + state, + TransitionType::OnExit, + ); + self + } + + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition( + self.main_mut(), + PhantomData::, + state, + TransitionType::OnEnter, + ); self } } impl StateScopedEventsAppExt for SubApp { - fn add_state_scoped_event(&mut self, state: impl FreelyMutableState) -> &mut Self { - add_state_scoped_event_impl(self, PhantomData::, state); + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnExit); + self + } + + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::StatesPlugin; + use bevy_ecs::event::{BufferedEvent, Event}; + use bevy_state::prelude::*; + + #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)] + enum TestState { + #[default] + A, + B, + } + + #[derive(Event, BufferedEvent, Debug)] + struct StandardEvent; + + #[derive(Event, BufferedEvent, Debug)] + struct StateScopedEvent; + + #[test] + fn clear_event_on_exit_state() { + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.init_state::(); + + app.add_event::(); + app.add_event::() + .clear_events_on_exit_state::(TestState::A); + + app.world_mut().send_event(StandardEvent).unwrap(); + app.world_mut().send_event(StateScopedEvent).unwrap(); + assert!(!app.world().resource::>().is_empty()); + assert!(!app + .world() + .resource::>() + .is_empty()); + + app.world_mut() + .resource_mut::>() + .set(TestState::B); + app.update(); + + assert!(!app.world().resource::>().is_empty()); + assert!(app + .world() + .resource::>() + .is_empty()); + } + + #[test] + fn clear_event_on_enter_state() { + let mut app = App::new(); + app.add_plugins(StatesPlugin); + app.init_state::(); + + app.add_event::(); + app.add_event::() + .clear_events_on_enter_state::(TestState::B); + + app.world_mut().send_event(StandardEvent).unwrap(); + app.world_mut().send_event(StateScopedEvent).unwrap(); + assert!(!app.world().resource::>().is_empty()); + assert!(!app + .world() + .resource::>() + .is_empty()); + + app.world_mut() + .resource_mut::>() + .set(TestState::B); + app.update(); + + assert!(!app.world().resource::>().is_empty()); + assert!(app + .world() + .resource::>() + .is_empty()); + } +} diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 07c20b9750..61b07448ed 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_tasks" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A task executor for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -15,7 +15,12 @@ default = ["std", "async_executor"] ## Enables multi-threading support. ## Without this feature, all tasks will be run on a single thread. -multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] +multi_threaded = [ + "std", + "dep:async-channel", + "dep:concurrent-queue", + "async_executor", +] ## Uses `async-executor` as a task execution backend. ## This backend is incompatible with `no_std` targets. @@ -42,7 +47,7 @@ web = [ ] [dependencies] -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "alloc", ] } diff --git a/crates/bevy_tasks/README.md b/crates/bevy_tasks/README.md index b03d2fcf97..04815df35e 100644 --- a/crates/bevy_tasks/README.md +++ b/crates/bevy_tasks/README.md @@ -38,6 +38,6 @@ The determining factor for what kind of work should go in each pool is latency r To enable `no_std` support in this crate, you will need to disable default features, and enable the `edge_executor` and `critical-section` features. -[bevy]: https://bevyengine.org +[bevy]: https://bevy.org [rayon]: https://github.com/rayon-rs/rayon [async-executor]: https://github.com/stjepang/async-executor diff --git a/crates/bevy_tasks/src/executor.rs b/crates/bevy_tasks/src/executor.rs index 9a9f4f9dfa..01bbe4a669 100644 --- a/crates/bevy_tasks/src/executor.rs +++ b/crates/bevy_tasks/src/executor.rs @@ -65,9 +65,11 @@ impl LocalExecutor<'_> { } impl UnwindSafe for Executor<'_> {} + impl RefUnwindSafe for Executor<'_> {} impl UnwindSafe for LocalExecutor<'_> {} + impl RefUnwindSafe for LocalExecutor<'_> {} impl fmt::Debug for Executor<'_> { diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index ae684a4eb5..66899ef36f 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -1,8 +1,8 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -32,6 +32,7 @@ pub use conditional_send::*; /// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. Wasm), /// futures aren't Send. pub trait ConditionalSendFuture: Future + ConditionalSend {} + impl ConditionalSendFuture for T {} use alloc::boxed::Box; diff --git a/crates/bevy_tasks/src/thread_executor.rs b/crates/bevy_tasks/src/thread_executor.rs index 48fb3e2861..86d2ab280d 100644 --- a/crates/bevy_tasks/src/thread_executor.rs +++ b/crates/bevy_tasks/src/thread_executor.rs @@ -24,7 +24,7 @@ use futures_lite::Future; /// // we cannot get the ticker from another thread /// let not_thread_ticker = thread_executor.ticker(); /// assert!(not_thread_ticker.is_none()); -/// +/// /// // but we can spawn tasks from another thread /// thread_executor.spawn(async move { /// count_clone.fetch_add(1, Ordering::Relaxed); @@ -98,6 +98,7 @@ pub struct ThreadExecutorTicker<'task, 'ticker> { // make type not send or sync _marker: PhantomData<*const ()>, } + impl<'task, 'ticker> ThreadExecutorTicker<'task, 'ticker> { /// Tick the thread executor. pub async fn tick(&self) { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 5134ecda84..c71fb5ce0c 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_text" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides text functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -13,21 +13,21 @@ default_font = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", ] } diff --git a/crates/bevy_text/src/bounds.rs b/crates/bevy_text/src/bounds.rs index db2ceb0b28..1c0833b443 100644 --- a/crates/bevy_text/src/bounds.rs +++ b/crates/bevy_text/src/bounds.rs @@ -5,7 +5,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// The maximum width and height of text. The text will wrap according to the specified size. /// /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the -/// specified [`JustifyText`](crate::text::JustifyText). +/// specified [`Justify`](crate::text::Justify). /// /// Note: only characters that are completely out of the bounds will be truncated, so this is not a /// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index c761bc0033..ebae713af4 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -23,7 +23,7 @@ pub struct PositionedGlyph { pub span_index: usize, /// The index of the glyph's line. pub line_index: usize, - /// The byte index of the glyph in it's line. + /// The byte index of the glyph in its line. pub byte_index: usize, /// The byte length of the glyph. pub byte_length: usize, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 70ac992924..b36f5fa2bb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -61,7 +61,7 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, JustifyText, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, + Font, Justify, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, TextFont, TextLayout, TextSpan, }; } @@ -87,18 +87,6 @@ pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); #[derive(Default)] pub struct TextPlugin; -/// Text is rendered for two different view projections; -/// 2-dimensional text ([`Text2d`]) is rendered in "world space" with a `BottomToTop` Y-axis, -/// while UI is rendered with a `TopToBottom` Y-axis. -/// This matters for text because the glyph positioning is different in either layout. -/// For `TopToBottom`, 0 is the top of the text, while for `BottomToTop` 0 is the bottom. -pub enum YAxisOrientation { - /// Top to bottom Y-axis orientation, for UI - TopToBottom, - /// Bottom to top Y-axis orientation, for 2d world space - BottomToTop, -} - /// System set in [`PostUpdate`] where all 2d text update systems are executed. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub struct Text2dUpdateSystems; diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index ebaa10b12b..dd6ca77246 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, - LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, YAxisOrientation, + error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -53,11 +53,15 @@ impl Default for SwashCache { /// Information about a font collected as part of preparing for text layout. #[derive(Clone)] -struct FontFaceInfo { - stretch: cosmic_text::fontdb::Stretch, - style: cosmic_text::fontdb::Style, - weight: cosmic_text::fontdb::Weight, - family_name: Arc, +pub struct FontFaceInfo { + /// Width class: + pub stretch: cosmic_text::fontdb::Stretch, + /// Allows italic or oblique faces to be selected + pub style: cosmic_text::fontdb::Style, + /// The degree of blackness or stroke thickness + pub weight: cosmic_text::fontdb::Weight, + /// Font family name + pub family_name: Arc, } /// The `TextPipeline` is used to layout and render text blocks (see `Text`/[`Text2d`](crate::Text2d)). @@ -66,7 +70,7 @@ struct FontFaceInfo { #[derive(Default, Resource)] pub struct TextPipeline { /// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset). - map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, Arc)>, + pub map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, Arc)>, /// Buffered vec for collecting spans. /// /// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10). @@ -84,7 +88,7 @@ impl TextPipeline { fonts: &Assets, text_spans: impl Iterator, linebreak: LineBreak, - justify: JustifyText, + justify: Justify, bounds: TextBounds, scale_factor: f64, computed: &mut ComputedTextBlock, @@ -197,7 +201,7 @@ impl TextPipeline { // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != JustifyText::Left { + if bounds.width.is_none() && justify != Justify::Left { let dimensions = buffer_dimensions(buffer); // `set_size` causes a re-layout to occur. buffer.set_size(font_system, Some(dimensions.x), bounds.height); @@ -228,7 +232,6 @@ impl TextPipeline { font_atlas_sets: &mut FontAtlasSets, texture_atlases: &mut Assets, textures: &mut Assets, - y_axis_orientation: YAxisOrientation, computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, swash_cache: &mut SwashCache, @@ -348,10 +351,6 @@ impl TextPipeline { let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; let y = line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; - let y = match y_axis_orientation { - YAxisOrientation::TopToBottom => y, - YAxisOrientation::BottomToTop => box_size.y - y, - }; let position = Vec2::new(x, y); @@ -489,7 +488,8 @@ impl TextMeasureInfo { } } -fn load_font_to_fontdb( +/// Add the font to the cosmic text's `FontSystem`'s in-memory font database +pub fn load_font_to_fontdb( text_font: &TextFont, font_system: &mut cosmic_text::FontSystem, map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, Arc)>, @@ -532,7 +532,7 @@ fn get_attrs<'a>( face_info: &'a FontFaceInfo, scale_factor: f64, ) -> Attrs<'a> { - let attrs = Attrs::new() + Attrs::new() .metadata(span_index) .family(Family::Name(&face_info.family_name)) .stretch(face_info.stretch) @@ -545,8 +545,7 @@ fn get_attrs<'a>( } .scale(scale_factor as f32), ) - .color(cosmic_text::Color(color.to_linear().as_u32())); - attrs + .color(cosmic_text::Color(color.to_linear().as_u32())) } /// Calculate the size of the text area for the given buffer. diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index e9e78e3ed2..ccfdb2a372 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,8 +1,3 @@ -pub use cosmic_text::{ - self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, - Weight as FontWeight, -}; - use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; @@ -121,19 +116,19 @@ impl Default for ComputedTextBlock { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: JustifyText, + pub justify: Justify, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self { + pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { Self { justify, linebreak } } - /// Makes a new [`TextLayout`] with the specified [`JustifyText`]. - pub fn new_with_justify(justify: JustifyText) -> Self { + /// Makes a new [`TextLayout`] with the specified [`Justify`]. + pub fn new_with_justify(justify: Justify) -> Self { Self::default().with_justify(justify) } @@ -148,8 +143,8 @@ impl TextLayout { Self::default().with_no_wrap() } - /// Returns this [`TextLayout`] with the specified [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { + /// Returns this [`TextLayout`] with the specified [`Justify`]. + pub const fn with_justify(mut self, justify: Justify) -> Self { self.justify = justify; self } @@ -251,7 +246,7 @@ impl From for TextSpan { /// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] -pub enum JustifyText { +pub enum Justify { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -268,13 +263,13 @@ pub enum JustifyText { Justified, } -impl From for cosmic_text::Align { - fn from(justify: JustifyText) -> Self { +impl From for cosmic_text::Align { + fn from(justify: Justify) -> Self { match justify { - JustifyText::Left => cosmic_text::Align::Left, - JustifyText::Center => cosmic_text::Align::Center, - JustifyText::Right => cosmic_text::Align::Right, - JustifyText::Justified => cosmic_text::Align::Justified, + Justify::Left => cosmic_text::Align::Left, + Justify::Center => cosmic_text::Align::Center, + Justify::Right => cosmic_text::Align::Right, + Justify::Justified => cosmic_text::Align::Justified, } } } @@ -359,8 +354,8 @@ impl Default for TextFont { /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size -#[derive(Debug, Clone, Copy, Reflect)] -#[reflect(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, Clone, PartialEq)] pub enum LineHeight { /// Set line height to a specific number of pixels Px(f32), diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a9419e89c0..8d4a926e1b 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -2,7 +2,7 @@ use crate::pipeline::CosmicFontSystem; use crate::{ ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, - TextSpanAccess, TextWriter, YAxisOrientation, + TextSpanAccess, TextWriter, }; use bevy_asset::Assets; use bevy_color::LinearRgba; @@ -51,7 +51,7 @@ use bevy_window::{PrimaryWindow, Window}; /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, Text2d, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, Text2d, TextLayout, TextFont, TextColor, TextSpan}; /// # /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); @@ -73,7 +73,7 @@ use bevy_window::{PrimaryWindow, Window}; /// // With text justification. /// world.spawn(( /// Text2d::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans @@ -182,10 +182,10 @@ pub fn extract_text2d_sprite( text_bounds.width.unwrap_or(text_layout_info.size.x), text_bounds.height.unwrap_or(text_layout_info.size.y), ); - let bottom_left = - -(anchor.as_vec() + 0.5) * size + (size.y - text_layout_info.size.y) * Vec2::Y; + + let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; let transform = - *global_transform * GlobalTransform::from_translation(bottom_left.extend(0.)) * scaling; + *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; let mut color = LinearRgba::WHITE; let mut current_span = usize::MAX; @@ -218,7 +218,7 @@ pub fn extract_text2d_sprite( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_slices.slices.push(ExtractedSlice { - offset: *position, + offset: Vec2::new(position.x, -position.y), rect, size: rect.size(), }); @@ -316,7 +316,6 @@ pub fn update_text2d_layout( &mut font_atlas_sets, &mut texture_atlases, &mut textures, - YAxisOrientation::BottomToTop, computed.as_mut(), &mut font_system, &mut swash_cache, diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 520782b519..7c8e840ace 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_time" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides time functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -48,10 +48,10 @@ critical-section = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other crossbeam-channel = { version = "0.5.0", default-features = false, features = [ diff --git a/crates/bevy_time/src/common_conditions.rs b/crates/bevy_time/src/common_conditions.rs index d944303439..cb0b30d13e 100644 --- a/crates/bevy_time/src/common_conditions.rs +++ b/crates/bevy_time/src/common_conditions.rs @@ -167,7 +167,7 @@ pub fn repeating_after_delay(duration: Duration) -> impl FnMut(Res

for SetGradientViewBindGroup { + type Param = SRes; + type ViewQuery = Read; + type ItemQuery = (); + + fn render<'w>( + _item: &P, + view_uniform: &'w ViewUniformOffset, + _entity: Option<()>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else { + return RenderCommandResult::Failure("view_bind_group not available"); + }; + pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]); + RenderCommandResult::Success + } +} + +pub struct DrawGradient; +impl RenderCommand

for DrawGradient { + type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: Option<&'w GradientBatch>, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let Some(batch) = batch else { + return RenderCommandResult::Skip; + }; + let ui_meta = ui_meta.into_inner(); + let Some(vertices) = ui_meta.vertices.buffer() else { + return RenderCommandResult::Failure("missing vertices to draw ui"); + }; + let Some(indices) = ui_meta.indices.buffer() else { + return RenderCommandResult::Failure("missing indices to draw ui"); + }; + + // Store the vertices + pass.set_vertex_buffer(0, vertices.slice(..)); + // Define how to "connect" the vertices + pass.set_index_buffer(indices.slice(..), 0, IndexFormat::Uint32); + // Draw the vertices + pass.draw_indexed(batch.range.clone(), 0, 0..1); + RenderCommandResult::Success + } +} diff --git a/crates/bevy_ui/src/render/gradient.wgsl b/crates/bevy_ui/src/render/gradient.wgsl new file mode 100644 index 0000000000..074cf35a35 --- /dev/null +++ b/crates/bevy_ui/src/render/gradient.wgsl @@ -0,0 +1,290 @@ +#import bevy_render::view::View +#import bevy_ui::ui_node::{ + draw_uinode_background, + draw_uinode_border, +} + +const PI: f32 = 3.14159265358979323846; +const TAU: f32 = 2. * PI; + +const TEXTURED = 1u; +const RIGHT_VERTEX = 2u; +const BOTTOM_VERTEX = 4u; +// must align with BORDER_* shader_flags from bevy_ui/render/mod.rs +const RADIAL: u32 = 16u; +const FILL_START: u32 = 32u; +const FILL_END: u32 = 64u; +const CONIC: u32 = 128u; +const BORDER_LEFT: u32 = 256u; +const BORDER_TOP: u32 = 512u; +const BORDER_RIGHT: u32 = 1024u; +const BORDER_BOTTOM: u32 = 2048u; +const BORDER_ANY: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM; + +fn enabled(flags: u32, mask: u32) -> bool { + return (flags & mask) != 0u; +} + +@group(0) @binding(0) var view: View; + +struct GradientVertexOutput { + @location(0) uv: vec2, + @location(1) @interpolate(flat) size: vec2, + @location(2) @interpolate(flat) flags: u32, + @location(3) @interpolate(flat) radius: vec4, + @location(4) @interpolate(flat) border: vec4, + + // Position relative to the center of the rectangle. + @location(5) point: vec2, + @location(6) @interpolate(flat) g_start: vec2, + @location(7) @interpolate(flat) dir: vec2, + @location(8) @interpolate(flat) start_color: vec4, + @location(9) @interpolate(flat) start_len: f32, + @location(10) @interpolate(flat) end_len: f32, + @location(11) @interpolate(flat) end_color: vec4, + @location(12) @interpolate(flat) hint: f32, + @builtin(position) position: vec4, +}; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2, + @location(2) flags: u32, + + // x: top left, y: top right, z: bottom right, w: bottom left. + @location(3) radius: vec4, + + // x: left, y: top, z: right, w: bottom. + @location(4) border: vec4, + @location(5) size: vec2, + @location(6) point: vec2, + @location(7) @interpolate(flat) g_start: vec2, + @location(8) @interpolate(flat) dir: vec2, + @location(9) @interpolate(flat) start_color: vec4, + @location(10) @interpolate(flat) start_len: f32, + @location(11) @interpolate(flat) end_len: f32, + @location(12) @interpolate(flat) end_color: vec4, + @location(13) @interpolate(flat) hint: f32 +) -> GradientVertexOutput { + var out: GradientVertexOutput; + out.position = view.clip_from_world * vec4(vertex_position, 1.0); + out.uv = vertex_uv; + out.size = size; + out.flags = flags; + out.radius = radius; + out.border = border; + out.point = point; + out.dir = dir; + out.start_color = start_color; + out.start_len = start_len; + out.end_len = end_len; + out.end_color = end_color; + out.g_start = g_start; + out.hint = hint; + + return out; +} + +@fragment +fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { + var g_distance: f32; + if enabled(in.flags, RADIAL) { + g_distance = radial_distance(in.point, in.g_start, in.dir.x); + } else if enabled(in.flags, CONIC) { + g_distance = conic_distance(in.dir.x, in.point, in.g_start); + } else { + g_distance = linear_distance(in.point, in.g_start, in.dir); + } + + let gradient_color = interpolate_gradient( + g_distance, + in.start_color, + in.start_len, + in.end_color, + in.end_len, + in.hint, + in.flags + ); + + if enabled(in.flags, BORDER_ANY) { + return draw_uinode_border(gradient_color, in.point, in.size, in.radius, in.border, in.flags); + } else { + return draw_uinode_background(gradient_color, in.point, in.size, in.radius, in.border); + } +} + +// This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space. +fn mix_linear_rgb_in_srgb_space(a: vec4, b: vec4, t: f32) -> vec4 { + let a_srgb = pow(a.rgb, vec3(1. / 2.2)); + let b_srgb = pow(b.rgb, vec3(1. / 2.2)); + let mixed_srgb = mix(a_srgb, b_srgb, t); + return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); +} + +fn linear_rgb_to_oklab(c: vec4) -> vec4 { + let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.); + let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.); + let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.); + return vec4( + 0.21045426 * l + 0.7936178 * m - 0.004072047 * s, + 1.9779985 * l - 2.4285922 * m + 0.4505937 * s, + 0.025904037 * l + 0.78277177 * m - 0.80867577 * s, + c.w + ); +} + +fn oklab_to_linear_rgba(c: vec4) -> vec4 { + let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; + let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; + let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z; + let l = l_ * l_ * l_; + let m = m_ * m_ * m_; + let s = s_ * s_ * s_; + return vec4( + 4.0767417 * l - 3.3077116 * m + 0.23096994 * s, + -1.268438 * l + 2.6097574 * m - 0.34131938 * s, + -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s, + c.w + ); +} + +fn mix_linear_rgb_in_oklab_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t)); +} + +/// hue is left in radians and not converted to degrees +fn linear_rgb_to_oklch(c: vec4) -> vec4 { + let o = linear_rgb_to_oklab(c); + let chroma = sqrt(o.y * o.y + o.z * o.z); + let hue = atan2(o.z, o.y); + return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w); +} + +fn oklch_to_linear_rgb(c: vec4) -> vec4 { + let a = c.y * cos(c.z); + let b = c.y * sin(c.z); + return oklab_to_linear_rgba(vec4(c.x, a, b, c.w)); +} + +fn rem_euclid(a: f32, b: f32) -> f32 { + return ((a % b) + b) % b; +} + +fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + diff * t, TAU); +} + +fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { + let diff = rem_euclid(b - a + PI, TAU) - PI; + return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU); +} + +fn mix_oklch(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_oklch_long(a: vec4, b: vec4, t: f32) -> vec4 { + return vec4( + mix(a.xy, b.xy, t), + lerp_hue_long(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_linear_rgb_in_oklch_space(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + +fn mix_linear_rgb_in_oklch_space_long(a: vec4, b: vec4, t: f32) -> vec4 { + return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); +} + +// These functions are used to calculate the distance in gradient space from the start of the gradient to the point. +// The distance in gradient space is then used to interpolate between the start and end colors. + +fn linear_distance( + point: vec2, + g_start: vec2, + g_dir: vec2, +) -> f32 { + return dot(point - g_start, g_dir); +} + +fn radial_distance( + point: vec2, + center: vec2, + ratio: f32, +) -> f32 { + let d = point - center; + return length(vec2(d.x, d.y * ratio)); +} + +fn conic_distance( + start: f32, + point: vec2, + center: vec2, +) -> f32 { + let d = point - center; + let angle = atan2(-d.x, d.y) + PI; + return (((angle - start) % TAU) + TAU) % TAU; +} + +fn interpolate_gradient( + distance: f32, + start_color: vec4, + start_distance: f32, + end_color: vec4, + end_distance: f32, + hint: f32, + flags: u32, +) -> vec4 { + if start_distance == end_distance { + if distance <= start_distance && enabled(flags, FILL_START) { + return start_color; + } + if start_distance <= distance && enabled(flags, FILL_END) { + return end_color; + } + return vec4(0.); + } + + var t = (distance - start_distance) / (end_distance - start_distance); + + if t < 0.0 { + if enabled(flags, FILL_START) { + return start_color; + } + return vec4(0.0); + } + + if 1. < t { + if enabled(flags, FILL_END) { + return end_color; + } + return vec4(0.0); + } + + if t < hint { + t = 0.5 * t / hint; + } else { + t = 0.5 * (1 + (t - hint) / (1.0 - hint)); + } + +#ifdef IN_SRGB + return mix_linear_rgb_in_srgb_space(start_color, end_color, t); +#else ifdef IN_OKLAB + return mix_linear_rgb_in_oklab_space(start_color, end_color, t); +#else ifdef IN_OKLCH + return mix_linear_rgb_in_oklch_space(start_color, end_color, t); +#else ifdef IN_OKLCH_LONG + return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t); +#else + return mix(start_color, end_color, t); +#endif +} diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index e811cfe362..61319eda9b 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -6,14 +6,17 @@ pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; +mod gradient; +use crate::prelude::UiGlobalTransform; use crate::widget::{ImageNode, ViewportNode}; + use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, }; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, weak_handle, AssetEvent, AssetId, Assets, Handle}; +use bevy_asset::{AssetEvent, AssetId, Assets}; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d}; use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; @@ -21,13 +24,14 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2}; +use bevy_render::load_shader_library; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::renderer::RenderContext; use bevy_render::sync_world::MainEntity; use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; -use bevy_render::view::RetainedViewEntity; +use bevy_render::view::{Hdr, RetainedViewEntity}; use bevy_render::{ camera::Camera, render_asset::RenderAssets, @@ -48,6 +52,7 @@ use bevy_render::{ use bevy_sprite::{BorderRect, SpriteAssetEvents}; #[cfg(feature = "bevy_ui_debug")] pub use debug_overlay::UiDebugOptions; +use gradient::GradientPlugin; use crate::{Display, Node}; use bevy_platform::collections::{HashMap, HashSet}; @@ -94,11 +99,10 @@ pub mod stack_z_offsets { pub const BOX_SHADOW: f32 = -0.1; pub const TEXTURE_SLICE: f32 = 0.0; pub const NODE: f32 = 0.0; + pub const GRADIENT: f32 = 0.1; pub const MATERIAL: f32 = 0.18267; } -pub const UI_SHADER_HANDLE: Handle = weak_handle!("7d190d05-545b-42f5-bd85-22a0da85b0f6"); - #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystems { ExtractCameraViews, @@ -112,6 +116,7 @@ pub enum RenderUiSystems { ExtractTextShadows, ExtractText, ExtractDebug, + ExtractGradient, } /// Deprecated alias for [`RenderUiSystems`]. @@ -119,7 +124,7 @@ pub enum RenderUiSystems { pub type RenderUiSystem = RenderUiSystems; pub fn build_ui_render(app: &mut App) { - load_internal_asset!(app, UI_SHADER_HANDLE, "ui.wgsl", Shader::from_wgsl); + load_shader_library!(app, "ui.wgsl"); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -196,6 +201,7 @@ pub fn build_ui_render(app: &mut App) { } app.add_plugins(UiTextureSlicerPlugin); + app.add_plugins(GradientPlugin); app.add_plugins(BoxShadowPlugin); } @@ -224,7 +230,7 @@ pub struct ExtractedUiNode { #[derive(Clone, Copy, Debug, PartialEq)] pub enum NodeType { Rect, - Border, + Border(u32), // shader flags } pub enum ExtractedUiItem { @@ -239,7 +245,7 @@ pub enum ExtractedUiItem { /// Ordering: left, top, right, bottom. border: BorderRect, node_type: NodeType, - transform: Mat4, + transform: Affine2, }, /// A contiguous sequence of text glyphs from the same section Glyphs { @@ -249,7 +255,7 @@ pub enum ExtractedUiItem { } pub struct ExtractedGlyph { - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, } @@ -340,7 +346,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -379,7 +385,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -399,7 +405,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -463,7 +469,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -483,7 +489,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -499,7 +505,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, + transform, inherited_visibility, maybe_clip, camera, @@ -517,30 +523,63 @@ pub fn extract_uinode_borders( // Don't extract borders with zero width along all edges if computed_node.border() != BorderRect::ZERO { - if let Some(border_color) = maybe_border_color.filter(|bc| !bc.0.is_fully_transparent()) - { - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index: computed_node.stack_index, - color: border_color.0.into(), - rect: Rect { - max: computed_node.size(), - ..Default::default() - }, - image, - clip: maybe_clip.map(|clip| clip.clip), - extracted_camera_entity, - item: ExtractedUiItem::Node { - atlas_scaling: None, - transform: global_transform.compute_matrix(), - flip_x: false, - flip_y: false, - border: computed_node.border(), - border_radius: computed_node.border_radius(), - node_type: NodeType::Border, - }, - main_entity: entity.into(), - render_entity: commands.spawn(TemporaryRenderEntity).id(), - }); + if let Some(border_color) = maybe_border_color { + let border_colors = [ + border_color.left.to_linear(), + border_color.top.to_linear(), + border_color.right.to_linear(), + border_color.bottom.to_linear(), + ]; + + const BORDER_FLAGS: [u32; 4] = [ + shader_flags::BORDER_LEFT, + shader_flags::BORDER_TOP, + shader_flags::BORDER_RIGHT, + shader_flags::BORDER_BOTTOM, + ]; + let mut completed_flags = 0; + + for (i, &color) in border_colors.iter().enumerate() { + if color.is_fully_transparent() { + continue; + } + + let mut border_flags = BORDER_FLAGS[i]; + + if completed_flags & border_flags != 0 { + continue; + } + + for j in i + 1..4 { + if color == border_colors[j] { + border_flags |= BORDER_FLAGS[j]; + } + } + completed_flags |= border_flags; + + extracted_uinodes.uinodes.push(ExtractedUiNode { + stack_index: computed_node.stack_index, + color, + rect: Rect { + max: computed_node.size(), + ..Default::default() + }, + image, + clip: maybe_clip.map(|clip| clip.clip), + extracted_camera_entity, + item: ExtractedUiItem::Node { + atlas_scaling: None, + transform: transform.into(), + flip_x: false, + flip_y: false, + border: computed_node.border(), + border_radius: computed_node.border_radius(), + node_type: NodeType::Border(border_flags), + }, + main_entity: entity.into(), + render_entity: commands.spawn(TemporaryRenderEntity).id(), + }); + } } } @@ -563,13 +602,13 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, border: BorderRect::all(computed_node.outline_width()), border_radius: computed_node.outline_radius(), - node_type: NodeType::Border, + node_type: NodeType::Border(shader_flags::BORDER_ALL), }, main_entity: entity.into(), }); @@ -624,6 +663,7 @@ pub fn extract_ui_camera_view( Entity, RenderEntity, &Camera, + Has, Option<&UiAntiAlias>, Option<&BoxShadowSamples>, ), @@ -634,7 +674,7 @@ pub fn extract_ui_camera_view( ) { live_entities.clear(); - for (main_entity, render_entity, camera, ui_anti_alias, shadow_samples) in &query { + for (main_entity, render_entity, camera, hdr, ui_anti_alias, shadow_samples) in &query { // ignore inactive cameras if !camera.is_active { commands @@ -670,7 +710,7 @@ pub fn extract_ui_camera_view( UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, ), clip_from_world: None, - hdr: camera.hdr, + hdr, viewport: UVec4::from(( physical_viewport_rect.min, physical_viewport_rect.size(), @@ -711,7 +751,7 @@ pub fn extract_viewport_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -754,7 +794,7 @@ pub fn extract_viewport_nodes( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -774,7 +814,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -792,7 +832,7 @@ pub fn extract_text_sections( for ( entity, uinode, - global_transform, + transform, inherited_visibility, clip, camera, @@ -809,8 +849,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -828,7 +867,7 @@ pub fn extract_text_sections( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -872,8 +911,8 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -886,16 +925,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -906,9 +937,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let node_transform = Affine2::from(*transform) + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -927,7 +958,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: node_transform * Affine2::from_translation(*position), rect, }); @@ -960,7 +991,7 @@ pub fn extract_text_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -983,8 +1014,8 @@ pub fn extract_text_background_colors( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for &(section_entity, rect) in text_layout_info.section_rects.iter() { let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { @@ -1004,7 +1035,7 @@ pub fn extract_text_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform * Mat4::from_translation(rect.center().extend(0.)), + transform: transform * Affine2::from_translation(rect.center()), flip_x: false, flip_y: false, border: uinode.border(), @@ -1055,11 +1086,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1076,7 +1107,15 @@ pub mod shader_flags { pub const TEXTURED: u32 = 1; /// Ordering: top left, top right, bottom right, bottom left. pub const CORNERS: [u32; 4] = [0, 2, 2 | 4, 4]; - pub const BORDER: u32 = 8; + pub const RADIAL: u32 = 16; + pub const FILL_START: u32 = 32; + pub const FILL_END: u32 = 64; + pub const CONIC: u32 = 128; + pub const BORDER_LEFT: u32 = 256; + pub const BORDER_TOP: u32 = 512; + pub const BORDER_RIGHT: u32 = 1024; + pub const BORDER_BOTTOM: u32 = 2048; + pub const BORDER_ALL: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM; } pub fn queue_uinodes( @@ -1275,12 +1314,12 @@ pub fn prepare_uinodes( let mut uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -1321,7 +1360,7 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1385,8 +1424,8 @@ pub fn prepare_uinodes( }; let color = extracted_uinode.color.to_f32_array(); - if *node_type == NodeType::Border { - flags |= shader_flags::BORDER; + if let NodeType::Border(border_flags) = *node_type { + flags |= border_flags; } for i in 0..4 { @@ -1402,7 +1441,7 @@ pub fn prepare_uinodes( border_radius.bottom_left, ], border: [border.left, border.top, border.right, border.bottom], - size: rect_size.xy().into(), + size: rect_size.into(), point: points[i].into(), }); } @@ -1424,13 +1463,14 @@ pub fn prepare_uinodes( let color = extracted_uinode.color.to_f32_array(); for glyph in &extracted_uinodes.glyphs[range.clone()] { let glyph_rect = glyph.rect; - let size = glyph.rect.size(); - - let rect_size = glyph_rect.size().extend(1.0); + let rect_size = glyph_rect.size(); // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1465,7 +1505,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y @@ -1502,7 +1542,7 @@ pub fn prepare_uinodes( flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], radius: [0.0; 4], border: [0.0; 4], - size: size.into(), + size: rect_size.into(), point: [0.0; 2], }); } diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index dd465515c5..c020e038fb 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -1,3 +1,4 @@ +use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::prelude::*; use bevy_image::BevyDefault as _; use bevy_render::{ @@ -13,6 +14,7 @@ use bevy_render::{ pub struct UiPipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub shader: Handle, } impl FromWorld for UiPipeline { @@ -41,6 +43,7 @@ impl FromWorld for UiPipeline { UiPipeline { view_layout, image_layout, + shader: load_embedded_asset!(world, "ui.wgsl"), } } } @@ -84,13 +87,13 @@ impl SpecializedRenderPipeline for UiPipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: super::UI_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], }, fragment: Some(FragmentState { - shader: super::UI_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index 3fd339405d..e7c7ec4350 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -1,9 +1,16 @@ +#define_import_path bevy_ui::ui_node + #import bevy_render::view::View const TEXTURED = 1u; const RIGHT_VERTEX = 2u; const BOTTOM_VERTEX = 4u; -const BORDER: u32 = 8u; +// must align with BORDER_* shader_flags from bevy_ui/render/mod.rs +const BORDER_LEFT: u32 = 256u; +const BORDER_TOP: u32 = 512u; +const BORDER_RIGHT: u32 = 1024u; +const BORDER_BOTTOM: u32 = 2048u; +const BORDER_ANY: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM; fn enabled(flags: u32, mask: u32) -> bool { return (flags & mask) != 0u; @@ -114,35 +121,62 @@ fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, in return sd_rounded_box(inner_point, inner_size, r); } +fn nearest_border_active(point_vs_mid: vec2, size: vec2, width: vec4, flags: u32) -> bool { + if (flags & BORDER_ANY) == BORDER_ANY { + return true; + } + + // get point vs top left + let point = clamp(point_vs_mid + size * 0.49999, vec2(0.0), size); + + let left = point.x / width.x; + let top = point.y / width.y; + let right = (size.x - point.x) / width.z; + let bottom = (size.y - point.y) / width.w; + + let min_dist = min(min(left, top), min(right, bottom)); + + return (enabled(flags, BORDER_LEFT) && min_dist == left) || + (enabled(flags, BORDER_TOP) && min_dist == top) || + (enabled(flags, BORDER_RIGHT) && min_dist == right) || + (enabled(flags, BORDER_BOTTOM) && min_dist == bottom); +} + // get alpha for antialiasing for sdf fn antialias(distance: f32) -> f32 { // Using the fwidth(distance) was causing artifacts, so just use the distance. return saturate(0.5 - distance); } -fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { - // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. - // This allows us to draw both textured and untextured shapes together in the same batch. - let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); - +fn draw_uinode_border( + color: vec4, + point: vec2, + size: vec2, + radius: vec4, + border: vec4, + flags: u32, +) -> vec4 { // Signed distances. The magnitude is the distance of the point from the edge of the shape. // * Negative values indicate that the point is inside the shape. // * Zero values indicate the point is on the edge of the shape. // * Positive values indicate the point is outside the shape. // Signed distance from the exterior boundary. - let external_distance = sd_rounded_box(in.point, in.size, in.radius); + let external_distance = sd_rounded_box(point, size, radius); // Signed distance from the border's internal edge (the signed distance is negative if the point // is inside the rect but not on the border). // If the border size is set to zero, this is the same as the external distance. - let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + let internal_distance = sd_inset_rounded_box(point, size, radius, border); // Signed distance from the border (the intersection of the rect with its border). // Points inside the border have negative signed distance. Any point outside the border, whether // outside the outside edge, or inside the inner edge have positive signed distance. let border_distance = max(external_distance, -internal_distance); + // check if this node should apply color for the nearest border + let nearest_border = select(0.0, 1.0, nearest_border_active(point, size, border, flags)); + #ifdef ANTI_ALIAS // At external edges with no border, `border_distance` is equal to zero. // This select statement ensures we only perform anti-aliasing where a non-zero width border @@ -154,14 +188,18 @@ fn draw(in: VertexOutput, texture_color: vec4) -> vec4 { #endif // Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here. - return vec4(color.rgb, saturate(color.a * t)); + return vec4(color.rgb, saturate(color.a * t * nearest_border)); } -fn draw_background(in: VertexOutput, texture_color: vec4) -> vec4 { - let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); - +fn draw_uinode_background( + color: vec4, + point: vec2, + size: vec2, + radius: vec4, + border: vec4, +) -> vec4 { // When drawing the background only draw the internal area and not the border. - let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); + let internal_distance = sd_inset_rounded_box(point, size, radius, border); #ifdef ANTI_ALIAS let t = antialias(internal_distance); @@ -176,9 +214,13 @@ fn draw_background(in: VertexOutput, texture_color: vec4) -> vec4 { fn fragment(in: VertexOutput) -> @location(0) vec4 { let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); - if enabled(in.flags, BORDER) { - return draw(in, texture_color); + // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. + // This allows us to draw both textured and untextured shapes together in the same batch. + let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); + + if enabled(in.flags, BORDER_ANY) { + return draw_uinode_border(color, in.point, in.size, in.radius, in.border, in.flags); } else { - return draw_background(in, texture_color); + return draw_uinode_background(color, in.point, in.size, in.radius, in.border); } } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 84eb163e4a..3ad4f4ea6a 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -1,9 +1,7 @@ -use core::{hash::Hash, marker::PhantomData, ops::Range}; - use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::Component, + prelude::{Component, With}, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -11,27 +9,22 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; -use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::{ extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, TemporaryRenderEntity}, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; - -pub const UI_MATERIAL_SHADER_HANDLE: Handle = - weak_handle!("b5612b7b-aed5-41b4-a930-1d1588239fcd"); - -const UI_VERTEX_OUTPUT_SHADER_HANDLE: Handle = - weak_handle!("1d97ca3e-eaa8-4bc5-a676-e8e9568c472e"); +use core::{hash::Hash, marker::PhantomData, ops::Range}; /// Adds the necessary ECS resources and render logic to enable rendering entities using the given /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). @@ -48,18 +41,10 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - UI_VERTEX_OUTPUT_SHADER_HANDLE, - "ui_vertex_output.wgsl", - Shader::from_wgsl - ); - load_internal_asset!( - app, - UI_MATERIAL_SHADER_HANDLE, - "ui_material.wgsl", - Shader::from_wgsl - ); + load_shader_library!(app, "ui_vertex_output.wgsl"); + + embedded_asset!(app, "ui_material.wgsl"); + app.init_asset::() .register_type::>() .add_plugins(( @@ -135,8 +120,8 @@ pub struct UiMaterialBatch { pub struct UiMaterialPipeline { pub ui_layout: BindGroupLayout, pub view_layout: BindGroupLayout, - pub vertex_shader: Option>, - pub fragment_shader: Option>, + pub vertex_shader: Handle, + pub fragment_shader: Handle, marker: PhantomData, } @@ -166,13 +151,13 @@ where let mut descriptor = RenderPipelineDescriptor { vertex: VertexState { - shader: UI_MATERIAL_SHADER_HANDLE, + shader: self.vertex_shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], }, fragment: Some(FragmentState { - shader: UI_MATERIAL_SHADER_HANDLE, + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -205,13 +190,6 @@ where label: Some("ui_material_pipeline".into()), zero_initialize_workgroup_memory: false, }; - if let Some(vertex_shader) = &self.vertex_shader { - descriptor.vertex.shader = vertex_shader.clone(); - } - - if let Some(fragment_shader) = &self.fragment_shader { - descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); - } descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()]; @@ -238,18 +216,20 @@ impl FromWorld for UiMaterialPipeline { ), ); + let load_default = || load_embedded_asset!(asset_server, "ui_material.wgsl"); + UiMaterialPipeline { ui_layout, view_layout, vertex_shader: match M::vertex_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), + ShaderRef::Default => load_default(), + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), }, fragment_shader: match M::fragment_shader() { - ShaderRef::Default => None, - ShaderRef::Handle(handle) => Some(handle), - ShaderRef::Path(path) => Some(asset_server.load(path)), + ShaderRef::Default => load_default(), + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), }, marker: PhantomData, } @@ -296,7 +276,7 @@ impl RenderCommand

fn render<'w>( _item: &P, _view: (), - material_handle: Option>, + material_handle: Option>, materials: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -337,7 +317,7 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, pub border_radius: ResolvedBorderRadius, @@ -372,7 +352,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -403,7 +383,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, @@ -475,10 +455,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -512,7 +495,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 7d0fdb6a42..0e232ab1cc 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; @@ -11,7 +12,7 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,24 +26,15 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; -pub const UI_SLICER_SHADER_HANDLE: Handle = - weak_handle!("10cd61e3-bbf7-47fa-91c8-16cbe806378c"); - pub struct UiTextureSlicerPlugin; impl Plugin for UiTextureSlicerPlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - UI_SLICER_SHADER_HANDLE, - "ui_texture_slice.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "ui_texture_slice.wgsl"); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -116,6 +108,7 @@ pub struct UiTextureSliceImageBindGroups { pub struct UiTextureSlicePipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub shader: Handle, } impl FromWorld for UiTextureSlicePipeline { @@ -144,6 +137,7 @@ impl FromWorld for UiTextureSlicePipeline { UiTextureSlicePipeline { view_layout, image_layout, + shader: load_embedded_asset!(world, "ui_texture_slice.wgsl"), } } } @@ -180,13 +174,13 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { RenderPipelineDescriptor { vertex: VertexState { - shader: UI_SLICER_SHADER_HANDLE, + shader: self.shader.clone(), entry_point: "vertex".into(), shader_defs: shader_defs.clone(), buffers: vec![vertex_layout], }, fragment: Some(FragmentState { - shader: UI_SLICER_SHADER_HANDLE, + shader: self.shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -224,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -252,7 +246,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -312,7 +306,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -503,11 +497,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -542,7 +537,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index c95859624e..6418f69ff8 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,5 +1,8 @@ -use crate::{FocusPolicy, UiRect, Val}; -use bevy_color::Color; +use crate::{ + ui_transform::{UiGlobalTransform, UiTransform}, + FocusPolicy, UiRect, Val, +}; +use bevy_color::{Alpha, Color}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_math::{vec4, Rect, UVec2, Vec2, Vec4Swizzles}; @@ -7,13 +10,11 @@ use bevy_reflect::prelude::*; use bevy_render::{ camera::{Camera, RenderTarget}, view::Visibility, - view::VisibilityClass, }; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; -use core::num::NonZero; +use core::{f32, num::NonZero}; use derive_more::derive::From; use smallvec::SmallVec; use thiserror::Error; @@ -162,8 +163,8 @@ impl ComputedNode { ResolvedBorderRadius { top_left: compute_radius(self.border_radius.top_left, outer_distance), top_right: compute_radius(self.border_radius.top_right, outer_distance), - bottom_left: compute_radius(self.border_radius.bottom_left, outer_distance), bottom_right: compute_radius(self.border_radius.bottom_right, outer_distance), + bottom_left: compute_radius(self.border_radius.bottom_left, outer_distance), } } @@ -200,8 +201,8 @@ impl ComputedNode { ResolvedBorderRadius { top_left: clamp_corner(self.border_radius.top_left, s, b.xy()), top_right: clamp_corner(self.border_radius.top_right, s, b.zy()), - bottom_left: clamp_corner(self.border_radius.bottom_right, s, b.zw()), bottom_right: clamp_corner(self.border_radius.bottom_left, s, b.xw()), + bottom_left: clamp_corner(self.border_radius.bottom_right, s, b.zw()), } } @@ -230,6 +231,73 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + // Returns true if `point` within the node. + // + // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. + pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { + let Some(local_point) = transform + .try_inverse() + .map(|transform| transform.transform_point2(point)) + else { + return false; + }; + let [top, bottom] = if local_point.x < 0. { + [self.border_radius.top_left, self.border_radius.bottom_left] + } else { + [ + self.border_radius.top_right, + self.border_radius.bottom_right, + ] + }; + let r = if local_point.y < 0. { top } else { bottom }; + let corner_to_point = local_point.abs() - 0.5 * self.size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. + } + + /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] + pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { + self.size + .cmpgt(Vec2::ZERO) + .all() + .then(|| transform.try_inverse()) + .flatten() + .map(|transform| transform.transform_point2(point) / self.size) + } + + /// Resolve the node's clipping rect in local space + pub fn resolve_clip_rect( + &self, + overflow: Overflow, + overflow_clip_margin: OverflowClipMargin, + ) -> Rect { + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, self.size); + + let clip_inset = match overflow_clip_margin.visual_box { + OverflowClipBox::BorderBox => BorderRect::ZERO, + OverflowClipBox::ContentBox => self.content_inset(), + OverflowClipBox::PaddingBox => self.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + clip_rect + } } impl ComputedNode { @@ -324,14 +392,13 @@ impl From for ScrollPosition { #[require( ComputedNode, ComputedNodeTarget, + UiTransform, BackgroundColor, BorderColor, BorderRadius, FocusPolicy, ScrollPosition, - Transform, Visibility, - VisibilityClass, ZIndex )] #[reflect(Component, Default, PartialEq, Debug, Clone)] @@ -976,9 +1043,11 @@ pub enum BoxSizing { /// Length styles like width and height refer to the "content box" size (size excluding padding and border) ContentBox, } + impl BoxSizing { pub const DEFAULT: Self = Self::BorderBox; } + impl Default for BoxSizing { fn default() -> Self { Self::DEFAULT @@ -2036,17 +2105,50 @@ impl> From for BackgroundColor { derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -pub struct BorderColor(pub Color); +pub struct BorderColor { + pub top: Color, + pub right: Color, + pub bottom: Color, + pub left: Color, +} impl> From for BorderColor { fn from(color: T) -> Self { - Self(color.into()) + Self::all(color.into()) } } impl BorderColor { /// Border color is transparent by default. - pub const DEFAULT: Self = BorderColor(Color::NONE); + pub const DEFAULT: Self = BorderColor::all(Color::NONE); + + /// Helper to create a `BorderColor` struct with all borders set to the given color + pub const fn all(color: Color) -> Self { + Self { + top: color, + bottom: color, + left: color, + right: color, + } + } + + /// Helper to set all border colors to a given color. + pub fn set_all(&mut self, color: impl Into) -> &mut Self { + let color: Color = color.into(); + self.top = color; + self.bottom = color; + self.left = color; + self.right = color; + self + } + + /// Check if all contained border colors are transparent + pub fn is_fully_transparent(&self) -> bool { + self.top.is_fully_transparent() + && self.bottom.is_fully_transparent() + && self.left.is_fully_transparent() + && self.right.is_fully_transparent() + } } impl Default for BorderColor { @@ -2217,8 +2319,8 @@ pub struct GlobalZIndex(pub i32); pub struct BorderRadius { pub top_left: Val, pub top_right: Val, - pub bottom_left: Val, pub bottom_right: Val, + pub bottom_left: Val, } impl Default for BorderRadius { @@ -2430,56 +2532,52 @@ impl BorderRadius { /// Resolve the border radius for a single corner from the given context values. /// Returns the radius of the corner in physical pixels. - pub fn resolve_single_corner( + pub const fn resolve_single_corner( radius: Val, - node_size: Vec2, - viewport_size: Vec2, scale_factor: f32, + min_length: f32, + viewport_size: Vec2, ) -> f32 { - match radius { - Val::Auto => 0., - Val::Px(px) => px * scale_factor, - Val::Percent(percent) => node_size.min_element() * percent / 100., - Val::Vw(percent) => viewport_size.x * percent / 100., - Val::Vh(percent) => viewport_size.y * percent / 100., - Val::VMin(percent) => viewport_size.min_element() * percent / 100., - Val::VMax(percent) => viewport_size.max_element() * percent / 100., + if let Ok(radius) = radius.resolve(scale_factor, min_length, viewport_size) { + radius.clamp(0., 0.5 * min_length) + } else { + 0. } - .clamp(0., 0.5 * node_size.min_element()) } /// Resolve the border radii for the corners from the given context values. /// Returns the radii of the each corner in physical pixels. - pub fn resolve( + pub const fn resolve( &self, + scale_factor: f32, node_size: Vec2, viewport_size: Vec2, - scale_factor: f32, ) -> ResolvedBorderRadius { + let length = node_size.x.min(node_size.y); ResolvedBorderRadius { top_left: Self::resolve_single_corner( self.top_left, - node_size, - viewport_size, scale_factor, + length, + viewport_size, ), top_right: Self::resolve_single_corner( self.top_right, - node_size, - viewport_size, scale_factor, + length, + viewport_size, ), bottom_left: Self::resolve_single_corner( self.bottom_left, - node_size, - viewport_size, scale_factor, + length, + viewport_size, ), bottom_right: Self::resolve_single_corner( self.bottom_right, - node_size, - viewport_size, scale_factor, + length, + viewport_size, ), } } @@ -2493,16 +2591,16 @@ impl BorderRadius { pub struct ResolvedBorderRadius { pub top_left: f32, pub top_right: f32, - pub bottom_left: f32, pub bottom_right: f32, + pub bottom_left: f32, } impl ResolvedBorderRadius { pub const ZERO: Self = Self { top_left: 0., top_right: 0., - bottom_left: 0., bottom_right: 0., + bottom_left: 0., }; } @@ -2600,37 +2698,6 @@ impl Default for LayoutConfig { } } -#[cfg(test)] -mod tests { - use crate::GridPlacement; - - #[test] - fn invalid_grid_placement_values() { - assert!(std::panic::catch_unwind(|| GridPlacement::span(0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::start(0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::end(0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::start_end(0, 1)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::start_end(-1, 0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::start_span(1, 0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::start_span(0, 1)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::end_span(0, 1)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::end_span(1, 0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::default().set_start(0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::default().set_end(0)).is_err()); - assert!(std::panic::catch_unwind(|| GridPlacement::default().set_span(0)).is_err()); - } - - #[test] - fn grid_placement_accessors() { - assert_eq!(GridPlacement::start(5).get_start(), Some(5)); - assert_eq!(GridPlacement::end(-4).get_end(), Some(-4)); - assert_eq!(GridPlacement::span(2).get_span(), Some(2)); - assert_eq!(GridPlacement::start_end(11, 21).get_span(), None); - assert_eq!(GridPlacement::start_span(3, 5).get_end(), None); - assert_eq!(GridPlacement::end_span(-4, 12).get_start(), None); - } -} - /// Indicates that this root [`Node`] entity should be rendered to a specific camera. /// /// UI then will be laid out respecting the camera's viewport and scale factor, and @@ -2810,8 +2877,10 @@ impl ComputedNodeTarget { } /// Adds a shadow behind text -#[derive(Component, Copy, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +/// +/// Not supported by `Text2d` +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct TextShadow { /// Shadow displacement in logical pixels /// With a value of zero the shadow will be hidden directly behind the text @@ -2828,3 +2897,34 @@ impl Default for TextShadow { } } } + +#[cfg(test)] +mod tests { + use crate::GridPlacement; + + #[test] + fn invalid_grid_placement_values() { + assert!(std::panic::catch_unwind(|| GridPlacement::span(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::end(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_end(0, 1)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_end(-1, 0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_span(1, 0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_span(0, 1)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::end_span(0, 1)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::end_span(1, 0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::default().set_start(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::default().set_end(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::default().set_span(0)).is_err()); + } + + #[test] + fn grid_placement_accessors() { + assert_eq!(GridPlacement::start(5).get_start(), Some(5)); + assert_eq!(GridPlacement::end(-4).get_end(), Some(-4)); + assert_eq!(GridPlacement::span(2).get_span(), Some(2)); + assert_eq!(GridPlacement::start_end(11, 21).get_span(), None); + assert_eq!(GridPlacement::start_span(3, 5).get_end(), None); + assert_eq!(GridPlacement::end_span(-4, 12).get_start(), None); + } +} diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs new file mode 100644 index 0000000000..47f8484e54 --- /dev/null +++ b/crates/bevy_ui/src/ui_transform.rs @@ -0,0 +1,191 @@ +use crate::Val; +use bevy_derive::Deref; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::Affine2; +use bevy_math::Rot2; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; + +/// A pair of [`Val`]s used to represent a 2-dimensional size or offset. +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Val2 { + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub x: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed height of the UI Node. + /// `Val::Auto` is resolved to `0.`. + pub y: Val, +} + +impl Val2 { + pub const ZERO: Self = Self { + x: Val::ZERO, + y: Val::ZERO, + }; + + /// Creates a new [`Val2`] where both components are in logical pixels + pub const fn px(x: f32, y: f32) -> Self { + Self { + x: Val::Px(x), + y: Val::Px(y), + } + } + + /// Creates a new [`Val2`] where both components are percentage values + pub const fn percent(x: f32, y: f32) -> Self { + Self { + x: Val::Percent(x), + y: Val::Percent(y), + } + } + + /// Creates a new [`Val2`] + pub const fn new(x: Val, y: Val) -> Self { + Self { x, y } + } + + /// Resolves this [`Val2`] from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. + /// + /// Component values of [`Val::Auto`] are resolved to 0. + pub fn resolve(&self, scale_factor: f32, base_size: Vec2, viewport_size: Vec2) -> Vec2 { + Vec2::new( + self.x + .resolve(scale_factor, base_size.x, viewport_size) + .unwrap_or(0.), + self.y + .resolve(scale_factor, base_size.y, viewport_size) + .unwrap_or(0.), + ) + } +} + +impl Default for Val2 { + fn default() -> Self { + Self::ZERO + } +} + +/// Relative 2D transform for UI nodes +/// +/// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[require(UiGlobalTransform)] +pub struct UiTransform { + /// Translate the node. + pub translation: Val2, + /// Scale the node. A negative value reflects the node in that axis. + pub scale: Vec2, + /// Rotate the node clockwise. + pub rotation: Rot2, +} + +impl UiTransform { + pub const IDENTITY: Self = Self { + translation: Val2::ZERO, + scale: Vec2::ONE, + rotation: Rot2::IDENTITY, + }; + + /// Creates a UI transform representing a rotation. + pub fn from_rotation(rotation: Rot2) -> Self { + Self { + rotation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a responsive translation. + pub fn from_translation(translation: Val2) -> Self { + Self { + translation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a scaling. + pub fn from_scale(scale: Vec2) -> Self { + Self { + scale, + ..Self::IDENTITY + } + } + + /// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size` + /// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale. + pub fn compute_affine(&self, scale_factor: f32, base_size: Vec2, target_size: Vec2) -> Affine2 { + Affine2::from_scale_angle_translation( + self.scale, + self.rotation.as_radians(), + self.translation + .resolve(scale_factor, base_size, target_size), + ) + } +} + +impl Default for UiTransform { + fn default() -> Self { + Self::IDENTITY + } +} + +/// Absolute 2D transform for UI nodes +/// +/// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) +/// in [`ui_layout_system`](crate::layout::ui_layout_system) +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiGlobalTransform(Affine2); + +impl Default for UiGlobalTransform { + fn default() -> Self { + Self(Affine2::IDENTITY) + } +} + +impl UiGlobalTransform { + /// If the transform is invertible returns its inverse. + /// Otherwise returns `None`. + #[inline] + pub fn try_inverse(&self) -> Option { + (self.matrix2.determinant() != 0.).then_some(self.inverse()) + } +} + +impl From for UiGlobalTransform { + fn from(value: Affine2) -> Self { + Self(value) + } +} + +impl From for Affine2 { + fn from(value: UiGlobalTransform) -> Self { + value.0 + } +} + +impl From<&UiGlobalTransform> for Affine2 { + fn from(value: &UiGlobalTransform) -> Self { + value.0 + } +} diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 7e27c4abdd..c0e9d09d7b 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,6 +2,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, UiTargetCamera, }; @@ -17,7 +18,6 @@ use bevy_ecs::{ use bevy_math::{Rect, UVec2}; use bevy_render::camera::Camera; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; /// Updates clipping for all nodes pub fn update_clipping_system( @@ -26,7 +26,7 @@ pub fn update_clipping_system( mut node_query: Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, ui_children: UiChildren, @@ -48,14 +48,13 @@ fn update_clipping( node_query: &mut Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = - node_query.get_mut(entity) + let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) else { return; }; @@ -91,10 +90,7 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = Rect::from_center_size( - global_transform.translation().truncate(), - computed_node.size(), - ); + let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index c65c4df354..9a743595b8 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -138,8 +138,8 @@ impl From> for ImageNode { } /// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image -#[derive(Default, Debug, Clone, Reflect)] -#[reflect(Clone, Default)] +#[derive(Default, Debug, Clone, PartialEq, Reflect)] +#[reflect(Clone, Default, PartialEq)] pub enum NodeImageMode { /// The image will be sized automatically by taking the size of the source image and applying any layout constraints. #[default] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 0153fa954c..d7f8e243a4 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -20,7 +20,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ scale_value, ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, - TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, YAxisOrientation, + TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -61,7 +61,7 @@ impl Default for TextNodeFlags { /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan}; /// # use bevy_ui::prelude::Text; /// # /// # let font_handle: Handle = Default::default(); @@ -84,7 +84,7 @@ impl Default for TextNodeFlags { /// // With text justification. /// world.spawn(( /// Text::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans @@ -328,7 +328,6 @@ fn queue_text( font_atlas_sets, texture_atlases, textures, - YAxisOrientation::TopToBottom, computed, font_system, swash_cache, diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 39ba4629a5..4e74e6ea94 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -1,40 +1,34 @@ [package] name = "bevy_utils" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A collection of utils for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["std", "serde"] +default = ["parallel"] -# Functionality +wgpu_wrapper = ["dep:send_wrapper"] -## Adds serialization support through `serde`. -serde = ["bevy_platform/serialize"] +# Provides access to the `Parallel` type. +parallel = ["bevy_platform/std", "dep:thread_local"] -# Platform Compatibility +std = ["disqualified/alloc"] -## Allows access to the `std` crate. Enabling this feature will prevent compilation -## on `no_std` targets, but provides access to certain additional features on -## supported platforms. -std = ["alloc", "bevy_platform/std", "dep:thread_local"] - -## Allows access to the `alloc` crate. -alloc = ["bevy_platform/alloc"] - -## `critical-section` provides the building blocks for synchronization primitives -## on all platforms, including `no_std`. -critical-section = ["bevy_platform/critical-section"] +debug = [] [dependencies] -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } +disqualified = { version = "1.0", default-features = false } thread_local = { version = "1.0", optional = true } +[target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] +send_wrapper = { version = "0.6.0", optional = true } + [dev-dependencies] static_assertions = "1.1.0" diff --git a/crates/bevy_utils/README.md b/crates/bevy_utils/README.md index 341755babc..4515209dbe 100644 --- a/crates/bevy_utils/README.md +++ b/crates/bevy_utils/README.md @@ -6,4 +6,4 @@ [![Docs](https://docs.rs/bevy_utils/badge.svg)](https://docs.rs/bevy_utils/latest/bevy_utils/) [![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) -A Collection of Utilities for the [Bevy Engine](https://bevyengine.org/). +A Collection of Utilities for the [Bevy Engine](https://bevy.org/). diff --git a/crates/bevy_utils/src/debug_info.rs b/crates/bevy_utils/src/debug_info.rs new file mode 100644 index 0000000000..c79c5ebe60 --- /dev/null +++ b/crates/bevy_utils/src/debug_info.rs @@ -0,0 +1,102 @@ +use alloc::{borrow::Cow, fmt, string::String}; +#[cfg(feature = "debug")] +use core::any::type_name; +use disqualified::ShortName; + +#[cfg(not(feature = "debug"))] +const FEATURE_DISABLED: &'static str = "Enable the debug feature to see the name"; + +/// Wrapper to help debugging ECS issues. This is used to display the names of systems, components, ... +/// +/// * If the `debug` feature is enabled, the actual name will be used +/// * If it is disabled, a string mentioning the disabled feature will be used +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DebugName { + #[cfg(feature = "debug")] + name: Cow<'static, str>, +} + +impl fmt::Display for DebugName { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "debug")] + f.write_str(self.name.as_ref())?; + #[cfg(not(feature = "debug"))] + f.write_str(FEATURE_DISABLED)?; + + Ok(()) + } +} + +impl DebugName { + /// Create a new `DebugName` from a `&str` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + pub fn borrowed(value: &'static str) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(value), + } + } + + /// Create a new `DebugName` from a `String` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + pub fn owned(value: String) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Owned(value), + } + } + + /// Create a new `DebugName` from a type by using its [`core::any::type_name`] + /// + /// The value will be ignored if the `debug` feature is not enabled + pub fn type_name() -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(type_name::()), + } + } + + /// Get the [`ShortName`] corresping to this debug name + /// + /// The value will be a static string if the `debug` feature is not enabled + pub fn shortname(&self) -> ShortName { + #[cfg(feature = "debug")] + return ShortName(self.name.as_ref()); + #[cfg(not(feature = "debug"))] + return ShortName(FEATURE_DISABLED); + } + + /// Return the string hold by this `DebugName` + /// + /// This is intended for debugging purpose, and only available if the `debug` feature is enabled + #[cfg(feature = "debug")] + pub fn as_string(&self) -> String { + self.name.clone().into_owned() + } +} + +impl From> for DebugName { + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + fn from(value: Cow<'static, str>) -> Self { + Self { + #[cfg(feature = "debug")] + name: value, + } + } +} + +impl From for DebugName { + fn from(value: String) -> Self { + Self::owned(value) + } +} + +impl From<&'static str> for DebugName { + fn from(value: &'static str) -> Self { + Self::borrowed(value) + } +} diff --git a/crates/bevy_utils/src/default.rs b/crates/bevy_utils/src/default.rs index 5b4b9fbdf9..0ca45d544d 100644 --- a/crates/bevy_utils/src/default.rs +++ b/crates/bevy_utils/src/default.rs @@ -12,7 +12,7 @@ /// } /// /// // Normally you would initialize a struct with defaults using "struct update syntax" -/// // combined with `Default::default()`. This example sets `Foo::bar` to 10 and the remaining +/// // combined with `Default::default()`. This example sets `Foo::a` to 10 and the remaining /// // values to their defaults. /// let foo = Foo { /// a: 10, diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 9f564f14c2..58979139bb 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -1,91 +1,69 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] //! General utilities for first-party [Bevy] engine crates. //! -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ -#[cfg(feature = "std")] -extern crate std; +/// Configuration information for this crate. +pub mod cfg { + pub(crate) use bevy_platform::cfg::*; -#[cfg(feature = "alloc")] -extern crate alloc; + pub use bevy_platform::cfg::{alloc, std}; + + define_alias! { + #[cfg(feature = "parallel")] => { + /// Indicates the `Parallel` type is available. + parallel + } + } +} + +cfg::std! { + extern crate std; +} + +cfg::alloc! { + extern crate alloc; + + mod map; + pub use map::*; +} + +cfg::parallel! { + mod parallel_queue; + pub use parallel_queue::*; +} /// The utilities prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { + pub use crate::debug_info::DebugName; pub use crate::default; } -pub mod synccell; -pub mod syncunsafecell; +#[cfg(feature = "wgpu_wrapper")] +mod wgpu_wrapper; +mod debug_info; mod default; mod once; -#[cfg(feature = "std")] -mod parallel_queue; #[doc(hidden)] pub use once::OnceFlag; pub use default::default; -#[cfg(feature = "std")] -pub use parallel_queue::*; +#[cfg(feature = "wgpu_wrapper")] +pub use wgpu_wrapper::WgpuWrapper; use core::mem::ManuallyDrop; -#[cfg(feature = "alloc")] -use { - bevy_platform::{ - collections::HashMap, - hash::{Hashed, NoOpHash, PassHash}, - }, - core::{any::TypeId, hash::Hash}, -}; - -/// A [`HashMap`] pre-configured to use [`Hashed`] keys and [`PassHash`] passthrough hashing. -/// Iteration order only depends on the order of insertions and deletions. -#[cfg(feature = "alloc")] -pub type PreHashMap = HashMap, V, PassHash>; - -/// Extension methods intended to add functionality to [`PreHashMap`]. -#[cfg(feature = "alloc")] -pub trait PreHashMapExt { - /// Tries to get or insert the value for the given `key` using the pre-computed hash first. - /// If the [`PreHashMap`] does not already contain the `key`, it will clone it and insert - /// the value returned by `func`. - fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V; -} - -#[cfg(feature = "alloc")] -impl PreHashMapExt for PreHashMap { - #[inline] - fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V { - use bevy_platform::collections::hash_map::RawEntryMut; - let entry = self - .raw_entry_mut() - .from_key_hashed_nocheck(key.hash(), key); - match entry { - RawEntryMut::Occupied(entry) => entry.into_mut(), - RawEntryMut::Vacant(entry) => { - let (_, value) = entry.insert_hashed_nocheck(key.hash(), key.clone(), func()); - value - } - } - } -} - -/// A specialized hashmap type with Key of [`TypeId`] -/// Iteration order only depends on the order of insertions and deletions. -#[cfg(feature = "alloc")] -pub type TypeIdMap = HashMap; - /// A type which calls a function when dropped. /// This can be used to ensure that cleanup code is run even in case of a panic. /// @@ -144,46 +122,3 @@ impl Drop for OnDrop { callback(); } } - -#[cfg(test)] -mod tests { - use super::*; - use static_assertions::assert_impl_all; - - // Check that the HashMaps are Clone if the key/values are Clone - assert_impl_all!(PreHashMap::: Clone); - - #[test] - fn fast_typeid_hash() { - struct Hasher; - - impl core::hash::Hasher for Hasher { - fn finish(&self) -> u64 { - 0 - } - fn write(&mut self, _: &[u8]) { - panic!("Hashing of core::any::TypeId changed"); - } - fn write_u64(&mut self, _: u64) {} - } - - Hash::hash(&TypeId::of::<()>(), &mut Hasher); - } - - #[cfg(feature = "alloc")] - #[test] - fn stable_hash_within_same_program_execution() { - use alloc::vec::Vec; - - let mut map_1 = >::default(); - let mut map_2 = >::default(); - for i in 1..10 { - map_1.insert(i, i); - map_2.insert(i, i); - } - assert_eq!( - map_1.iter().collect::>(), - map_2.iter().collect::>() - ); - } -} diff --git a/crates/bevy_utils/src/map.rs b/crates/bevy_utils/src/map.rs new file mode 100644 index 0000000000..3b54a357aa --- /dev/null +++ b/crates/bevy_utils/src/map.rs @@ -0,0 +1,155 @@ +use core::{any::TypeId, hash::Hash}; + +use bevy_platform::{ + collections::{hash_map::Entry, HashMap}, + hash::{Hashed, NoOpHash, PassHash}, +}; + +/// A [`HashMap`] pre-configured to use [`Hashed`] keys and [`PassHash`] passthrough hashing. +/// Iteration order only depends on the order of insertions and deletions. +pub type PreHashMap = HashMap, V, PassHash>; + +/// Extension methods intended to add functionality to [`PreHashMap`]. +pub trait PreHashMapExt { + /// Tries to get or insert the value for the given `key` using the pre-computed hash first. + /// If the [`PreHashMap`] does not already contain the `key`, it will clone it and insert + /// the value returned by `func`. + fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V; +} + +impl PreHashMapExt for PreHashMap { + #[inline] + fn get_or_insert_with V>(&mut self, key: &Hashed, func: F) -> &mut V { + use bevy_platform::collections::hash_map::RawEntryMut; + let entry = self + .raw_entry_mut() + .from_key_hashed_nocheck(key.hash(), key); + match entry { + RawEntryMut::Occupied(entry) => entry.into_mut(), + RawEntryMut::Vacant(entry) => { + let (_, value) = entry.insert_hashed_nocheck(key.hash(), key.clone(), func()); + value + } + } + } +} + +/// A specialized hashmap type with Key of [`TypeId`] +/// Iteration order only depends on the order of insertions and deletions. +pub type TypeIdMap = HashMap; + +/// Extension trait to make use of [`TypeIdMap`] more ergonomic. +/// +/// Each function on this trait is a trivial wrapper for a function +/// on [`HashMap`], replacing a `TypeId` key with a +/// generic parameter `T`. +/// +/// # Examples +/// +/// ```rust +/// # use std::any::TypeId; +/// # use bevy_utils::TypeIdMap; +/// use bevy_utils::TypeIdMapExt; +/// +/// struct MyType; +/// +/// // Using the built-in `HashMap` functions requires manually looking up `TypeId`s. +/// let mut map = TypeIdMap::default(); +/// map.insert(TypeId::of::(), 7); +/// assert_eq!(map.get(&TypeId::of::()), Some(&7)); +/// +/// // Using `TypeIdMapExt` functions does the lookup for you. +/// map.insert_type::(7); +/// assert_eq!(map.get_type::(), Some(&7)); +/// ``` +pub trait TypeIdMapExt { + /// Inserts a value for the type `T`. + /// + /// If the map did not previously contain this key then [`None`] is returned, + /// otherwise the value for this key is updated and the old value returned. + fn insert_type(&mut self, v: V) -> Option; + + /// Returns a reference to the value for type `T`, if one exists. + fn get_type(&self) -> Option<&V>; + + /// Returns a mutable reference to the value for type `T`, if one exists. + fn get_type_mut(&mut self) -> Option<&mut V>; + + /// Removes type `T` from the map, returning the value for this + /// key if it was previously present. + fn remove_type(&mut self) -> Option; + + /// Gets the type `T`'s entry in the map for in-place manipulation. + fn entry_type(&mut self) -> Entry<'_, TypeId, V, NoOpHash>; +} + +impl TypeIdMapExt for TypeIdMap { + #[inline] + fn insert_type(&mut self, v: V) -> Option { + self.insert(TypeId::of::(), v) + } + + #[inline] + fn get_type(&self) -> Option<&V> { + self.get(&TypeId::of::()) + } + + #[inline] + fn get_type_mut(&mut self) -> Option<&mut V> { + self.get_mut(&TypeId::of::()) + } + + #[inline] + fn remove_type(&mut self) -> Option { + self.remove(&TypeId::of::()) + } + + #[inline] + fn entry_type(&mut self) -> Entry<'_, TypeId, V, NoOpHash> { + self.entry(TypeId::of::()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use static_assertions::assert_impl_all; + + // Check that the HashMaps are Clone if the key/values are Clone + assert_impl_all!(PreHashMap::: Clone); + + #[test] + fn fast_typeid_hash() { + struct Hasher; + + impl core::hash::Hasher for Hasher { + fn finish(&self) -> u64 { + 0 + } + fn write(&mut self, _: &[u8]) { + panic!("Hashing of core::any::TypeId changed"); + } + fn write_u64(&mut self, _: u64) {} + } + + Hash::hash(&TypeId::of::<()>(), &mut Hasher); + } + + crate::cfg::alloc! { + #[test] + fn stable_hash_within_same_program_execution() { + use alloc::vec::Vec; + + let mut map_1 = >::default(); + let mut map_2 = >::default(); + for i in 1..10 { + map_1.insert(i, i); + map_2.insert(i, i); + } + assert_eq!( + map_1.iter().collect::>(), + map_2.iter().collect::>() + ); + } + } +} \ No newline at end of file diff --git a/crates/bevy_utils/src/parallel_queue.rs b/crates/bevy_utils/src/parallel_queue.rs index 861d17bcf2..e97a48378d 100644 --- a/crates/bevy_utils/src/parallel_queue.rs +++ b/crates/bevy_utils/src/parallel_queue.rs @@ -10,7 +10,6 @@ pub struct Parallel { locals: ThreadLocal>, } -/// A scope guard of a `Parallel`, when this struct is dropped ,the value will writeback to its `Parallel` impl Parallel { /// Gets a mutable iterator over all of the per-thread queues. pub fn iter_mut(&mut self) -> impl Iterator { @@ -34,7 +33,7 @@ impl Parallel { /// Mutably borrows the thread-local value. /// - /// If there is no thread-local value, it will be initialized to it's default. + /// If there is no thread-local value, it will be initialized to its default. pub fn borrow_local_mut(&self) -> impl DerefMut + '_ { self.locals.get_or_default().borrow_mut() } @@ -56,7 +55,6 @@ where } } -#[cfg(feature = "alloc")] impl Parallel> { /// Collect all enqueued items from all threads and appends them to the end of a /// single Vec. diff --git a/crates/bevy_utils/src/wgpu_wrapper.rs b/crates/bevy_utils/src/wgpu_wrapper.rs new file mode 100644 index 0000000000..272d0dd4c0 --- /dev/null +++ b/crates/bevy_utils/src/wgpu_wrapper.rs @@ -0,0 +1,50 @@ +/// A wrapper to safely make `wgpu` types Send / Sync on web with atomics enabled. +/// +/// On web with `atomics` enabled the inner value can only be accessed +/// or dropped on the `wgpu` thread or else a panic will occur. +/// On other platforms the wrapper simply contains the wrapped value. +#[derive(Debug, Clone)] +pub struct WgpuWrapper( + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] T, + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] send_wrapper::SendWrapper, +); + +// SAFETY: SendWrapper is always Send + Sync. +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[expect(unsafe_code, reason = "Blanket-impl Send requires unsafe.")] +unsafe impl Send for WgpuWrapper {} +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +#[expect(unsafe_code, reason = "Blanket-impl Sync requires unsafe.")] +unsafe impl Sync for WgpuWrapper {} + +impl WgpuWrapper { + /// Constructs a new instance of `WgpuWrapper` which will wrap the specified value. + pub fn new(t: T) -> Self { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return Self(t); + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return Self(send_wrapper::SendWrapper::new(t)); + } + + /// Unwraps the value. + pub fn into_inner(self) -> T { + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + return self.0; + #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] + return self.0.take(); + } +} + +impl core::ops::Deref for WgpuWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::ops::DerefMut for WgpuWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index b2b6d730fe..1a5bca6916 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_window" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "Provides windowing functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -50,16 +50,16 @@ libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features = false } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "glam", "smol_str", ], optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false } # other serde = { version = "1.0", features = [ diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 026b85dc32..89f219d269 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,5 +1,8 @@ use alloc::string::String; -use bevy_ecs::{entity::Entity, event::Event}; +use bevy_ecs::{ + entity::Entity, + event::{BufferedEvent, Event}, +}; use bevy_input::{ gestures::*, keyboard::{KeyboardFocusLost, KeyboardInput}, @@ -23,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use crate::WindowTheme; /// A window event that is sent whenever a window's logical size has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -45,7 +48,7 @@ pub struct WindowResized { /// An event that indicates all of the application's windows should be redrawn, /// even if their control flow is set to `Wait` and there have been no window events. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -61,7 +64,7 @@ pub struct RequestRedraw; /// An event that is sent whenever a new window is created. /// /// To create a new window, spawn an entity with a [`crate::Window`] on it. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -87,7 +90,7 @@ pub struct WindowCreated { /// /// [`WindowPlugin`]: crate::WindowPlugin /// [`Window`]: crate::Window -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -105,7 +108,7 @@ pub struct WindowCloseRequested { /// An event that is sent whenever a window is closed. This will be sent when /// the window entity loses its [`Window`](crate::window::Window) component or is despawned. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -126,7 +129,7 @@ pub struct WindowClosed { /// An event that is sent whenever a window is closing. This will be sent when /// after a [`WindowCloseRequested`] event is received and the window is in the process of closing. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -146,7 +149,7 @@ pub struct WindowClosing { /// /// Note that if your application only has a single window, this event may be your last chance to /// persist state before the application terminates. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -176,7 +179,7 @@ pub struct WindowDestroyed { /// you should not use it for non-cursor-like behavior such as 3D camera control. Please see `MouseMotion` instead. /// /// [`WindowEvent::CursorMoved`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.CursorMoved -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -201,7 +204,7 @@ pub struct CursorMoved { } /// An event that is sent whenever the user's cursor enters a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -218,7 +221,7 @@ pub struct CursorEntered { } /// An event that is sent whenever the user's cursor leaves a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -239,7 +242,7 @@ pub struct CursorLeft { /// This event is the translated version of the `WindowEvent::Ime` from the `winit` crate. /// /// It is only sent if IME was enabled on the window with [`Window::ime_enabled`](crate::window::Window::ime_enabled). -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -284,7 +287,7 @@ pub enum Ime { } /// An event that indicates a window has received or lost focus. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -311,7 +314,7 @@ pub struct WindowFocused { /// It is the translated version of [`WindowEvent::Occluded`] from the `winit` crate. /// /// [`WindowEvent::Occluded`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.Occluded -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -330,7 +333,7 @@ pub struct WindowOccluded { } /// An event that indicates a window's scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -349,7 +352,7 @@ pub struct WindowScaleFactorChanged { } /// An event that indicates a window's OS-reported scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -368,7 +371,7 @@ pub struct WindowBackendScaleFactorChanged { } /// Events related to files being dragged and dropped on a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -404,7 +407,7 @@ pub enum FileDragAndDrop { } /// An event that is sent when a window is repositioned in physical pixels. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -426,7 +429,7 @@ pub struct WindowMoved { /// /// This event is only sent when the window is relying on the system theme to control its appearance. /// i.e. It is only sent when [`Window::window_theme`](crate::window::Window::window_theme) is `None` and the system theme changes. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -445,7 +448,7 @@ pub struct WindowThemeChanged { } /// Application lifetime events -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -488,7 +491,7 @@ impl AppLifecycle { /// access window events in the order they were received from the /// operating system. Otherwise, the event types are individually /// readable with `EventReader` (e.g. `EventReader`). -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -499,38 +502,66 @@ impl AppLifecycle { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] pub enum WindowEvent { + /// An application lifecycle event. AppLifecycle(AppLifecycle), + /// The user's cursor has entered a window. CursorEntered(CursorEntered), + ///The user's cursor has left a window. CursorLeft(CursorLeft), + /// The user's cursor has moved inside a window. CursorMoved(CursorMoved), + /// A file drag and drop event. FileDragAndDrop(FileDragAndDrop), + /// An Input Method Editor event. Ime(Ime), + /// A redraw of all of the application's windows has been requested. RequestRedraw(RequestRedraw), + /// The window's OS-reported scale factor has changed. WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + /// The OS has requested that a window be closed. WindowCloseRequested(WindowCloseRequested), + /// A new window has been created. WindowCreated(WindowCreated), + /// A window has been destroyed by the underlying windowing system. WindowDestroyed(WindowDestroyed), + /// A window has received or lost focus. WindowFocused(WindowFocused), + /// A window has been moved. WindowMoved(WindowMoved), + /// A window has started or stopped being occluded. WindowOccluded(WindowOccluded), + /// A window's logical size has changed. WindowResized(WindowResized), + /// A window's scale factor has changed. WindowScaleFactorChanged(WindowScaleFactorChanged), + /// Sent for windows that are using the system theme when the system theme changes. WindowThemeChanged(WindowThemeChanged), + /// The state of a mouse button has changed. MouseButtonInput(MouseButtonInput), + /// The physical position of a pointing device has changed. MouseMotion(MouseMotion), + /// The mouse wheel has moved. MouseWheel(MouseWheel), + /// A two finger pinch gesture. PinchGesture(PinchGesture), + /// A two finger rotation gesture. RotationGesture(RotationGesture), + /// A double tap gesture. DoubleTapGesture(DoubleTapGesture), + /// A pan gesture. PanGesture(PanGesture), + /// A touch input state change. TouchInput(TouchInput), + /// A keyboard input. KeyboardInput(KeyboardInput), + /// Sent when focus has been lost for all Bevy windows. + /// + /// Used to clear pressed key state. KeyboardFocusLost(KeyboardFocusLost), } @@ -539,131 +570,157 @@ impl From for WindowEvent { Self::AppLifecycle(e) } } + impl From for WindowEvent { fn from(e: CursorEntered) -> Self { Self::CursorEntered(e) } } + impl From for WindowEvent { fn from(e: CursorLeft) -> Self { Self::CursorLeft(e) } } + impl From for WindowEvent { fn from(e: CursorMoved) -> Self { Self::CursorMoved(e) } } + impl From for WindowEvent { fn from(e: FileDragAndDrop) -> Self { Self::FileDragAndDrop(e) } } + impl From for WindowEvent { fn from(e: Ime) -> Self { Self::Ime(e) } } + impl From for WindowEvent { fn from(e: RequestRedraw) -> Self { Self::RequestRedraw(e) } } + impl From for WindowEvent { fn from(e: WindowBackendScaleFactorChanged) -> Self { Self::WindowBackendScaleFactorChanged(e) } } + impl From for WindowEvent { fn from(e: WindowCloseRequested) -> Self { Self::WindowCloseRequested(e) } } + impl From for WindowEvent { fn from(e: WindowCreated) -> Self { Self::WindowCreated(e) } } + impl From for WindowEvent { fn from(e: WindowDestroyed) -> Self { Self::WindowDestroyed(e) } } + impl From for WindowEvent { fn from(e: WindowFocused) -> Self { Self::WindowFocused(e) } } + impl From for WindowEvent { fn from(e: WindowMoved) -> Self { Self::WindowMoved(e) } } + impl From for WindowEvent { fn from(e: WindowOccluded) -> Self { Self::WindowOccluded(e) } } + impl From for WindowEvent { fn from(e: WindowResized) -> Self { Self::WindowResized(e) } } + impl From for WindowEvent { fn from(e: WindowScaleFactorChanged) -> Self { Self::WindowScaleFactorChanged(e) } } + impl From for WindowEvent { fn from(e: WindowThemeChanged) -> Self { Self::WindowThemeChanged(e) } } + impl From for WindowEvent { fn from(e: MouseButtonInput) -> Self { Self::MouseButtonInput(e) } } + impl From for WindowEvent { fn from(e: MouseMotion) -> Self { Self::MouseMotion(e) } } + impl From for WindowEvent { fn from(e: MouseWheel) -> Self { Self::MouseWheel(e) } } + impl From for WindowEvent { fn from(e: PinchGesture) -> Self { Self::PinchGesture(e) } } + impl From for WindowEvent { fn from(e: RotationGesture) -> Self { Self::RotationGesture(e) } } + impl From for WindowEvent { fn from(e: DoubleTapGesture) -> Self { Self::DoubleTapGesture(e) } } + impl From for WindowEvent { fn from(e: PanGesture) -> Self { Self::PanGesture(e) } } + impl From for WindowEvent { fn from(e: TouchInput) -> Self { Self::TouchInput(e) } } + impl From for WindowEvent { fn from(e: KeyboardInput) -> Self { Self::KeyboardInput(e) } } + impl From for WindowEvent { fn from(e: KeyboardFocusLost) -> Self { Self::KeyboardFocusLost(e) diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 21ee8d64c9..22e657cf03 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -1,7 +1,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] #![no_std] @@ -57,6 +57,7 @@ impl Default for WindowPlugin { fn default() -> Self { WindowPlugin { primary_window: Some(Window::default()), + primary_cursor_options: Some(CursorOptions::default()), exit_condition: ExitCondition::OnAllClosed, close_when_requested: true, } @@ -76,6 +77,13 @@ pub struct WindowPlugin { /// [`exit_on_all_closed`]. pub primary_window: Option, + /// Settings for the cursor on the primary window. + /// + /// Defaults to `Some(CursorOptions::default())`. + /// + /// Has no effect if [`WindowPlugin::primary_window`] is `None`. + pub primary_cursor_options: Option, + /// Whether to exit the app when there are no open windows. /// /// If disabling this, ensure that you send the [`bevy_app::AppExit`] @@ -122,10 +130,14 @@ impl Plugin for WindowPlugin { .add_event::(); if let Some(primary_window) = &self.primary_window { - app.world_mut().spawn(primary_window.clone()).insert(( + let mut entity_commands = app.world_mut().spawn(primary_window.clone()); + entity_commands.insert(( PrimaryWindow, RawHandleWrapperHolder(Arc::new(Mutex::new(None))), )); + if let Some(primary_cursor_options) = &self.primary_cursor_options { + entity_commands.insert(primary_cursor_options.clone()); + } } match self.exit_condition { @@ -168,7 +180,8 @@ impl Plugin for WindowPlugin { // Register window descriptor and related types #[cfg(feature = "bevy_reflect")] app.register_type::() - .register_type::(); + .register_type::() + .register_type::(); } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 31ff212ebe..403801e9d0 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -158,10 +158,8 @@ impl ContainsEntity for NormalizedWindowRef { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] +#[require(CursorOptions)] pub struct Window { - /// The cursor options of this window. Cursor icons are set with the `Cursor` component on the - /// window entity. - pub cursor_options: CursorOptions, /// What presentation mode to give the window. pub present_mode: PresentMode, /// Which fullscreen or windowing mode should be used. @@ -225,6 +223,15 @@ pub struct Window { /// You should also set the window `composite_alpha_mode` to `CompositeAlphaMode::PostMultiplied`. pub transparent: bool, /// Get/set whether the window is focused. + /// + /// It cannot be set unfocused after creation. + /// + /// ## Platform-specific + /// + /// - iOS / Android / X11 / Wayland: Spawning unfocused is + /// [not supported](https://docs.rs/winit/latest/winit/window/struct.WindowAttributes.html#method.with_active). + /// - iOS / Android / Web / Wayland: Setting focused after creation is + /// [not supported](https://docs.rs/winit/latest/winit/window/struct.Window.html#method.focus_window). pub focused: bool, /// Where should the window appear relative to other overlapping window. /// @@ -461,7 +468,6 @@ impl Default for Window { Self { title: DEFAULT_WINDOW_TITLE.to_owned(), name: None, - cursor_options: Default::default(), present_mode: Default::default(), mode: Default::default(), position: Default::default(), @@ -719,11 +725,11 @@ impl WindowResizeConstraints { } /// Cursor data for a [`Window`]. -#[derive(Debug, Clone)] +#[derive(Component, Debug, Clone)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), - reflect(Debug, Default, Clone) + reflect(Component, Debug, Default, Clone) )] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -1162,13 +1168,17 @@ pub enum MonitorSelection { /// References an exclusive fullscreen video mode. /// /// Used when setting [`WindowMode::Fullscreen`] on a window. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Clone)] pub enum VideoModeSelection { /// Uses the video mode that the monitor is already in. Current, diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 5665a96029..bb28b84fb0 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "bevy_winit" -version = "0.16.0-dev" +version = "0.17.0-dev" edition = "2024" description = "A winit window and input backend for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] @@ -28,30 +28,30 @@ custom_cursor = ["bevy_image", "bevy_asset", "bytemuck", "wgpu-types"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } -bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_a11y = { path = "../bevy_a11y", version = "0.17.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.17.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", ] } # bevy optional -bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev", optional = true } -bevy_image = { path = "../bevy_image", version = "0.16.0-dev", optional = true } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev", optional = true } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev", optional = true } # other # feature rwh_06 refers to window_raw_handle@v0.6 winit = { version = "0.30", default-features = false, features = ["rwh_06"] } -accesskit_winit = { version = "0.25", default-features = false, features = [ +accesskit_winit = { version = "0.27", default-features = false, features = [ "rwh_06", ] } approx = { version = "0.5", default-features = false } @@ -60,7 +60,7 @@ raw-window-handle = "0.6" serde = { version = "1.0", features = ["derive"], optional = true } bytemuck = { version = "1.5", optional = true } wgpu-types = { version = "24", optional = true } -accesskit = "0.18" +accesskit = "0.19" tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -68,16 +68,16 @@ wasm-bindgen = { version = "0.2" } web-sys = "0.3" crossbeam-channel = "0.5" # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. -bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ +bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ +bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "web", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [ "web", ] } diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index bdca3f8585..c5c5e489a6 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,11 +22,12 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - observer::Trigger, + lifecycle::Remove, + observer::On, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, - world::{OnRemove, Ref}, + world::Ref, }; #[cfg(feature = "custom_cursor")] use bevy_image::{Image, TextureAtlasLayout}; @@ -191,7 +192,7 @@ fn update_cursors( } /// Resets the cursor to the default icon when `CursorIcon` is removed. -fn on_remove_cursor_icon(trigger: Trigger, mut commands: Commands) { +fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands .entity(trigger.target()) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index d7c880a9b9..8926095dc0 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![forbid(unsafe_code)] #![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" )] //! `bevy_winit` provides utilities to handle window creation and the eventloop through [`winit`] @@ -25,8 +25,8 @@ use winit::{event_loop::EventLoop, window::WindowId}; use bevy_a11y::AccessibilityRequested; use bevy_app::{App, Last, Plugin}; use bevy_ecs::prelude::*; -use bevy_window::{exit_on_all_closed, Window, WindowCreated}; -use system::{changed_windows, check_keyboard_focus_lost, despawn_windows}; +use bevy_window::{exit_on_all_closed, CursorOptions, Window, WindowCreated}; +use system::{changed_cursor_options, changed_windows, check_keyboard_focus_lost, despawn_windows}; pub use system::{create_monitors, create_windows}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub use winit::platform::web::CustomCursorExtWebSys; @@ -55,7 +55,9 @@ mod winit_monitors; mod winit_windows; thread_local! { - static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; + /// Temporary storage of WinitWindows data to replace usage of `!Send` resources. This will be replaced with proper + /// storage of `!Send` data after issue #17667 is complete. + pub static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; } /// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input @@ -69,9 +71,9 @@ thread_local! { /// in systems. /// /// When using eg. `MinimalPlugins` you can add this using `WinitPlugin::::default()`, where -/// `WakeUp` is the default `Event` that bevy uses. +/// `WakeUp` is the default event that bevy uses. #[derive(Default)] -pub struct WinitPlugin { +pub struct WinitPlugin { /// Allows the window (and the event loop) to be created on any thread /// instead of only the main thread. /// @@ -85,7 +87,7 @@ pub struct WinitPlugin { marker: PhantomData, } -impl Plugin for WinitPlugin { +impl Plugin for WinitPlugin { fn name(&self) -> &str { "bevy_winit::WinitPlugin" } @@ -140,6 +142,7 @@ impl Plugin for WinitPlugin { // `exit_on_all_closed` only checks if windows exist but doesn't access data, // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), + changed_cursor_options, despawn_windows, check_keyboard_focus_lost, ) @@ -153,7 +156,7 @@ impl Plugin for WinitPlugin { /// The default event that can be used to wake the window loop /// Wakes up the loop if in wait state -#[derive(Debug, Default, Clone, Copy, Event, Reflect)] +#[derive(Debug, Default, Clone, Copy, Event, BufferedEvent, Reflect)] #[reflect(Debug, Default, Clone)] pub struct WakeUp; @@ -164,7 +167,7 @@ pub struct WakeUp; /// /// When you receive this event it has already been handled by Bevy's main loop. /// Sending these events will NOT cause them to be processed by Bevy. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] pub struct RawWinitWindowEvent { /// The window for which the event was fired. pub window_id: WindowId, @@ -209,6 +212,7 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( ( Entity, &'static mut Window, + &'static CursorOptions, Option<&'static RawHandleWrapperHolder>, ), F, diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 1855539cc5..5b873d4620 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -46,7 +46,7 @@ use bevy_window::{ WindowScaleFactorChanged, WindowThemeChanged, }; #[cfg(target_os = "android")] -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use bevy_window::{CursorOptions, PrimaryWindow, RawHandleWrapper}; use crate::{ accessibility::ACCESS_KIT_ADAPTERS, @@ -58,7 +58,7 @@ use crate::{ /// Persistent state that is used to run the [`App`] according to the current /// [`UpdateMode`]. -struct WinitAppRunnerState { +struct WinitAppRunnerState { /// The running app. app: App, /// Exit value once the loop is finished. @@ -106,10 +106,11 @@ struct WinitAppRunnerState { )>, } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn new(mut app: App) -> Self { + app.add_event::(); #[cfg(feature = "custom_cursor")] - app.add_event::().init_resource::(); + app.init_resource::(); let event_writer_system_state: SystemState<( EventWriter, @@ -197,7 +198,7 @@ pub enum CursorSource { #[derive(Component, Debug)] pub struct PendingCursor(pub Option); -impl ApplicationHandler for WinitAppRunnerState { +impl ApplicationHandler for WinitAppRunnerState { fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { if event_loop.exiting() { return; @@ -473,7 +474,7 @@ impl ApplicationHandler for WinitAppRunnerState { if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) { if window_component.is_changed() { - cache.window = window_component.clone(); + **cache = window_component.clone(); } } }); @@ -548,7 +549,7 @@ impl ApplicationHandler for WinitAppRunnerState { } } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn redraw_requested(&mut self, event_loop: &ActiveEventLoop) { let mut redraw_event_reader = EventCursor::::default(); @@ -604,10 +605,12 @@ impl WinitAppRunnerState { { // Get windows that are cached but without raw handles. Those window were already created, but got their // handle wrapper removed when the app was suspended. + let mut query = self.world_mut() - .query_filtered::<(Entity, &Window), (With, Without)>(); - if let Ok((entity, window)) = query.single(&self.world()) { + .query_filtered::<(Entity, &Window, &CursorOptions), (With, Without)>(); + if let Ok((entity, window, cursor_options)) = query.single(&self.world()) { let window = window.clone(); + let cursor_options = cursor_options.clone(); WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { @@ -621,6 +624,7 @@ impl WinitAppRunnerState { event_loop, entity, &window, + &cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -933,7 +937,7 @@ impl WinitAppRunnerState { /// /// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the /// `EventLoop`. -pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { +pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { if app.plugins_state() == PluginsState::Ready { app.finish(); app.cleanup(); diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 97483c7358..3cc7c5a5f6 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,18 +1,20 @@ use std::collections::HashMap; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::Entity, event::EventWriter, + lifecycle::RemovedComponents, prelude::{Changed, Component}, query::QueryFilter, - removal_detection::RemovedComponents, system::{Local, NonSendMarker, Query, SystemParamItem}, }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_window::{ - ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, - WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, WindowResized, - WindowWrapper, + ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, + WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, + WindowResized, WindowWrapper, }; use tracing::{error, info, warn}; @@ -59,7 +61,7 @@ pub fn create_windows( ) { WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { - for (entity, mut window, handle_holder) in &mut created_windows { + for (entity, mut window, cursor_options, handle_holder) in &mut created_windows { if winit_windows.get_window(entity).is_some() { continue; } @@ -70,6 +72,7 @@ pub fn create_windows( event_loop, entity, &window, + cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -85,9 +88,8 @@ pub fn create_windows( .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); commands.entity(entity).insert(( - CachedWindow { - window: window.clone(), - }, + CachedWindow(window.clone()), + CachedCursorOptions(cursor_options.clone()), WinitWindowPressedKeys::default(), )); @@ -281,10 +283,12 @@ pub(crate) fn despawn_windows( } /// The cached state of the window so we can check which properties were changed from within the app. -#[derive(Debug, Clone, Component)] -pub struct CachedWindow { - pub window: Window, -} +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedWindow(Window); + +/// The cached state of the window so we can check which properties were changed from within the app. +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedCursorOptions(CursorOptions); /// Propagates changes from [`Window`] entities to the [`winit`] backend. /// @@ -306,11 +310,11 @@ pub(crate) fn changed_windows( continue; }; - if window.title != cache.window.title { + if window.title != cache.title { winit_window.set_title(window.title.as_str()); } - if window.mode != cache.window.mode { + if window.mode != cache.mode { let new_mode = match window.mode { WindowMode::BorderlessFullscreen(monitor_selection) => { Some(Some(winit::window::Fullscreen::Borderless(select_monitor( @@ -352,15 +356,15 @@ pub(crate) fn changed_windows( } } - if window.resolution != cache.window.resolution { + if window.resolution != cache.resolution { let mut physical_size = PhysicalSize::new( window.resolution.physical_width(), window.resolution.physical_height(), ); let cached_physical_size = PhysicalSize::new( - cache.window.physical_width(), - cache.window.physical_height(), + cache.physical_width(), + cache.physical_height(), ); let base_scale_factor = window.resolution.base_scale_factor(); @@ -368,12 +372,12 @@ pub(crate) fn changed_windows( // Note: this may be different from `winit`'s base scale factor if // `scale_factor_override` is set to Some(f32) let scale_factor = window.scale_factor(); - let cached_scale_factor = cache.window.scale_factor(); + let cached_scale_factor = cache.scale_factor(); // Check and update `winit`'s physical size only if the window is not maximized if scale_factor != cached_scale_factor && !winit_window.is_maximized() { let logical_size = - if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { + if let Some(cached_factor) = cache.resolution.scale_factor_override() { physical_size.to_logical::(cached_factor as f64) } else { physical_size.to_logical::(base_scale_factor as f64) @@ -397,7 +401,7 @@ pub(crate) fn changed_windows( } } - if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if window.physical_cursor_position() != cache.physical_cursor_position() { if let Some(physical_position) = window.physical_cursor_position() { let position = PhysicalPosition::new(physical_position.x, physical_position.y); @@ -407,44 +411,23 @@ pub(crate) fn changed_windows( } } - if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode - && crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode) - .is_err() - { - window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode; - } - - if window.cursor_options.visible != cache.window.cursor_options.visible { - winit_window.set_cursor_visible(window.cursor_options.visible); - } - - if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { - window.cursor_options.hit_test = cache.window.cursor_options.hit_test; - warn!( - "Could not set cursor hit test for window {}: {}", - window.title, err - ); - } - } - - if window.decorations != cache.window.decorations + if window.decorations != cache.decorations && window.decorations != winit_window.is_decorated() { winit_window.set_decorations(window.decorations); } - if window.resizable != cache.window.resizable + if window.resizable != cache.resizable && window.resizable != winit_window.is_resizable() { winit_window.set_resizable(window.resizable); } - if window.enabled_buttons != cache.window.enabled_buttons { + if window.enabled_buttons != cache.enabled_buttons { winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); } - if window.resize_constraints != cache.window.resize_constraints { + if window.resize_constraints != cache.resize_constraints { let constraints = window.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, @@ -461,7 +444,7 @@ pub(crate) fn changed_windows( } } - if window.position != cache.window.position { + if window.position != cache.position { if let Some(position) = crate::winit_window_position( &window.position, &window.resolution, @@ -502,62 +485,62 @@ pub(crate) fn changed_windows( } } - if window.focused != cache.window.focused && window.focused { + if window.focused != cache.focused && window.focused { winit_window.focus_window(); } - if window.window_level != cache.window.window_level { + if window.window_level != cache.window_level { winit_window.set_window_level(convert_window_level(window.window_level)); } // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; + if window.transparent != cache.transparent { + window.transparent = cache.transparent; warn!("Winit does not currently support updating transparency after window creation."); } #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas.clone_from(&cache.window.canvas); + if window.canvas != cache.canvas { + window.canvas.clone_from(&cache.canvas); warn!( "Bevy currently doesn't support modifying the window canvas after initialization." ); } - if window.ime_enabled != cache.window.ime_enabled { + if window.ime_enabled != cache.ime_enabled { winit_window.set_ime_allowed(window.ime_enabled); } - if window.ime_position != cache.window.ime_position { + if window.ime_position != cache.ime_position { winit_window.set_ime_cursor_area( LogicalPosition::new(window.ime_position.x, window.ime_position.y), PhysicalSize::new(10, 10), ); } - if window.window_theme != cache.window.window_theme { + if window.window_theme != cache.window_theme { winit_window.set_theme(window.window_theme.map(convert_window_theme)); } - if window.visible != cache.window.visible { + if window.visible != cache.visible { winit_window.set_visible(window.visible); } #[cfg(target_os = "ios")] { - if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { + if window.recognize_pinch_gesture != cache.recognize_pinch_gesture { winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); } - if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { + if window.recognize_rotation_gesture != cache.recognize_rotation_gesture { winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); } - if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { + if window.recognize_doubletap_gesture != cache.recognize_doubletap_gesture { winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); } - if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { + if window.recognize_pan_gesture != cache.recognize_pan_gesture { match ( window.recognize_pan_gesture, - cache.window.recognize_pan_gesture, + cache.recognize_pan_gesture, ) { (Some(_), Some(_)) => { warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); @@ -567,16 +550,15 @@ pub(crate) fn changed_windows( } } - if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { + if window.prefers_home_indicator_hidden != cache.prefers_home_indicator_hidden { winit_window .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); } - if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { + if window.prefers_status_bar_hidden != cache.prefers_status_bar_hidden { winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); } if window.preferred_screen_edges_deferring_system_gestures != cache - .window .preferred_screen_edges_deferring_system_gestures { use crate::converters::convert_screen_edge; @@ -585,7 +567,59 @@ pub(crate) fn changed_windows( winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); } } - cache.window = window.clone(); + **cache = window.clone(); + } + }); +} + +pub(crate) fn changed_cursor_options( + mut changed_windows: Query< + ( + Entity, + &Window, + &mut CursorOptions, + &mut CachedCursorOptions, + ), + Changed, + >, + _non_send_marker: NonSendMarker, +) { + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, window, mut cursor_options, mut cache) in &mut changed_windows { + // This system already only runs when the cursor options change, so we need to bypass change detection or the next frame will also run this system + let cursor_options = cursor_options.bypass_change_detection(); + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + // Don't check the cache for the grab mode. It can change through external means, leaving the cache outdated. + if let Err(err) = + crate::winit_windows::attempt_grab(winit_window, cursor_options.grab_mode) + { + warn!( + "Could not set cursor grab mode for window {}: {}", + window.title, err + ); + cursor_options.grab_mode = cache.grab_mode; + } else { + cache.grab_mode = cursor_options.grab_mode; + } + + if cursor_options.visible != cache.visible { + winit_window.set_cursor_visible(cursor_options.visible); + cache.visible = cursor_options.visible; + } + + if cursor_options.hit_test != cache.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { + warn!( + "Could not set cursor hit test for window {}: {}", + window.title, err + ); + cursor_options.hit_test = cache.hit_test; + } else { + cache.hit_test = cursor_options.hit_test; + } + } } }); } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index d666491311..5110e670c2 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -4,8 +4,8 @@ use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; use bevy_platform::collections::HashMap; use bevy_window::{ - CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition, - WindowResolution, WindowWrapper, + CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode, + WindowPosition, WindowResolution, WindowWrapper, }; use tracing::warn; @@ -58,6 +58,7 @@ impl WinitWindows { event_loop: &ActiveEventLoop, entity: Entity, window: &Window, + cursor_options: &CursorOptions, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, @@ -129,7 +130,8 @@ impl WinitWindows { .with_resizable(window.resizable) .with_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)) .with_decorations(window.decorations) - .with_transparent(window.transparent); + .with_transparent(window.transparent) + .with_active(window.focused); #[cfg(target_os = "windows")] { @@ -309,16 +311,16 @@ impl WinitWindows { winit_window.set_visible(window.visible); // Do not set the grab mode on window creation if it's none. It can fail on mobile. - if window.cursor_options.grab_mode != CursorGrabMode::None { - let _ = attempt_grab(&winit_window, window.cursor_options.grab_mode); + if cursor_options.grab_mode != CursorGrabMode::None { + let _ = attempt_grab(&winit_window, cursor_options.grab_mode); } - winit_window.set_cursor_visible(window.cursor_options.visible); + winit_window.set_cursor_visible(cursor_options.visible); // Do not set the cursor hittest on window creation if it's false, as it will always fail on // some platforms and log an unfixable warning. - if !window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { + if !cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { warn!( "Could not set cursor hit test for window {}: {}", window.title, err diff --git a/docs-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index a5438464b3..dfd516303b 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -269,6 +269,7 @@ Bevy has a helper to build its examples: - Build for WebGL2: `cargo run -p build-wasm-example -- --api webgl2 load_gltf` - Build for WebGPU: `cargo run -p build-wasm-example -- --api webgpu load_gltf` +- Debug: `cargo run -p build-wasm-example -- --debug --api webgl2 load_gltf` This helper will log the command used to build the examples. @@ -282,7 +283,7 @@ In browsers, audio is not authorized to start without being triggered by an user On the web, it's useful to reduce the size of the files that are distributed. With rust, there are many ways to improve your executable sizes, starting with -the steps described in [the quick-start guide](https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). +the steps described in [the quick-start guide](https://bevy.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). Now, when building the executable, use `--profile wasm-release` instead of `--release`: diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f15fa1c4c6..5998d8cb90 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -21,6 +21,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_color|Provides shared color types and operations| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_core_widgets|Headless widget collection for Bevy UI.| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| @@ -40,6 +41,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_window|Windowing layer| |bevy_winit|winit window and input backend| |custom_cursor|Enable winit custom cursor support| +|debug|Enable collecting debug information about systems and components to help with diagnostics| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| |ktx2|KTX2 compressed texture support| @@ -68,9 +70,9 @@ The default feature set enables most of the expected features of a game engine, |bevy_dev_tools|Provides a collection of developer tools| |bevy_image|Load and access image data. Usually added by an image format| |bevy_remote|Enable the Bevy Remote Protocol| +|bevy_solari|Provides raytraced lighting (experimental)| |bevy_ui_debug|Provides a debug overlay for bevy UI| |bmp|BMP image format support| -|configurable_error_handler|Use the configurable global error handler as the default error handler.| |critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.| |dds|DDS compressed texture support| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| @@ -86,6 +88,7 @@ The default feature set enables most of the expected features of a game engine, |ghost_nodes|Experimental support for nodes that are ignored for UI layouting| |gif|GIF image format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| +|hotpatching|Enable hotpatching of Bevy systems| |ico|ICO image format support| |jpeg|JPEG image format support| |libm|Uses the `libm` maths library instead of the one provided in `std` and `core`.| diff --git a/docs/linux_dependencies.md b/docs/linux_dependencies.md index 53ad5257e8..a5a43c3d5f 100644 --- a/docs/linux_dependencies.md +++ b/docs/linux_dependencies.md @@ -91,10 +91,10 @@ Set the `PKG_CONFIG_PATH` env var to `/usr/lib//pkgconfig/`. For example export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig/" ``` -## Arch / Manjaro +## [Arch](https://archlinux.org/) / [Manjaro](https://manjaro.org/) ```bash -sudo pacman -S libx11 pkgconf alsa-lib +sudo pacman -S libx11 pkgconf alsa-lib libxcursor libxrandr libxi ``` Install `pipewire-alsa` or `pulseaudio-alsa` depending on the sound server you are using. @@ -102,7 +102,7 @@ Install `pipewire-alsa` or `pulseaudio-alsa` depending on the sound server you a Depending on your graphics card, you may have to install one of the following: `vulkan-radeon`, `vulkan-intel`, or `mesa-vulkan-drivers` -## Void +## [Void](https://voidlinux.org/) ```bash sudo xbps-install -S pkgconf alsa-lib-devel libX11-devel eudev-libudev-devel @@ -110,6 +110,80 @@ sudo xbps-install -S pkgconf alsa-lib-devel libX11-devel eudev-libudev-devel ## [Nix](https://nixos.org) +### flake.nix + +Add a `flake.nix` file to the root of your GitHub repository containing: + +```nix +{ + description = "bevy flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + rust-overlay, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + in + { + devShells.default = + with pkgs; + mkShell { + buildInputs = + [ + # Rust dependencies + (rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; }) + pkg-config + ] + ++ lib.optionals (lib.strings.hasInfix "linux" system) [ + # for Linux + # Audio (Linux only) + alsa-lib + # Cross Platform 3D Graphics API + vulkan-loader + # For debugging around vulkan + vulkan-tools + # Other dependencies + libudev-zero + xorg.libX11 + xorg.libXcursor + xorg.libXi + xorg.libXrandr + libxkbcommon + ]; + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + LD_LIBRARY_PATH = lib.makeLibraryPath [ + vulkan-loader + xorg.libX11 + xorg.libXi + xorg.libXcursor + libxkbcommon + ]; + }; + } + ); +} +``` + +> [!TIP] +> We have confirmed that this flake.nix can be used successfully on NixOS and MacOS with Rust's edition set to 2021. + +### shell.nix + Add a `shell.nix` file to the root of the project containing: ```nix @@ -138,8 +212,8 @@ If running nix on a non NixOS system (such as ubuntu, arch etc.), [NixGL](https: to link graphics drivers into the context of software installed by nix: 1. Install a system specific nixGL wrapper ([docs](https://github.com/nix-community/nixGL)). - * If you're running a nvidia GPU choose `nixVulkanNvidia`. - * Otherwise, choose another wrapper appropriate for your system. + - If you're running a nvidia GPU choose `nixVulkanNvidia`. + - Otherwise, choose another wrapper appropriate for your system. 2. Run `nixVulkanNvidia-xxx.xxx.xx cargo run` to compile a bevy program, where `xxx-xxx-xx` denotes the graphics driver version `nixVulkanNvidia` was compiled with. This is also possible with [Nix flakes](https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html). @@ -152,7 +226,7 @@ for more information about `devShells`. Note that this template does not add Rust to the environment because there are many ways to do it. For example, to use stable Rust from nixpkgs, you can add `cargo` and `rustc` to `nativeBuildInputs`. -[Here]([https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/jumpy/package.nix](https://github.com/NixOS/nixpkgs/blob/0da3c44a9460a26d2025ec3ed2ec60a895eb1114/pkgs/games/jumpy/default.nix)) +[Here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/jumpy/package.nix) is an example of packaging a Bevy program in nix. ## [OpenSUSE](https://www.opensuse.org/) @@ -161,7 +235,7 @@ is an example of packaging a Bevy program in nix. sudo zypper install libudev-devel gcc-c++ alsa-lib-devel ``` -## Gentoo +## [Gentoo](https://www.gentoo.org/) ```bash sudo emerge --ask libX11 pkgconf alsa-lib diff --git a/errors/README.md b/errors/README.md index be2adb2b00..fe25e78400 100644 --- a/errors/README.md +++ b/errors/README.md @@ -3,6 +3,6 @@ This crate lists and tests explanations and examples of Bevy's error codes. For the latest Bevy release, you can find a rendered version of the error code descriptions at -[bevyengine.org/learn/errors]. +[bevy.org/learn/errors]. -[bevyengine.org/learn/errors]: https://bevyengine.org/learn/errors +[bevy.org/learn/errors]: https://bevy.org/learn/errors diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index dbdd846eba..f69e138364 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -1,4 +1,14 @@ -//! Shows how to render simple primitive shapes with a single color. +//! Here we use shape primitives to build meshes in a 2D rendering context, making each mesh a certain color by giving that mesh's entity a material based off a [`Color`]. +//! +//! Meshes are better known for their use in 3D rendering, but we can use them in a 2D context too. Without a third dimension, the meshes we're building are flat – like paper on a table. These are still very useful for "vector-style" graphics, picking behavior, or as a foundation to build off of for where to apply a shader. +//! +//! A "shape definition" is not a mesh on its own. A circle can be defined with a radius, i.e. [`Circle::new(50.0)`][Circle::new], but rendering tends to happen with meshes built out of triangles. So we need to turn shape descriptions into meshes. +//! +//! Thankfully, we can add shape primitives directly to [`Assets`] because [`Mesh`] implements [`From`] for shape primitives and [`Assets::add`] can be given any value that can be "turned into" `T`! +//! +//! We apply a material to the shape by first making a [`Color`] then calling [`Assets::add`] with that color as its argument, which will create a material from that color through the same process [`Assets::add`] can take a shape primitive. +//! +//! Both the mesh and material need to be wrapped in their own "newtypes". The mesh and material are currently [`Handle`] and [`Handle`] at the moment, which are not components. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh2d`] and [`MeshMaterial2d`] respectively. //! //! You can toggle wireframes with the space bar except on wasm. Wasm does not support //! `POLYGON_MODE_LINE` on the gpu. diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index fd90210d6f..9d9be1e5c7 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -25,7 +25,6 @@ fn setup( commands.spawn(( Camera2d, Camera { - hdr: true, // 1. HDR is required for bloom clear_color: ClearColorConfig::Custom(Color::BLACK), ..default() }, diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index c549134419..9cffb8e00c 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -129,7 +129,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { cmd.with_children(|builder| { builder.spawn(( Text2d::new(rect.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -275,7 +275,7 @@ fn setup_texture_atlas( cmd.with_children(|builder| { builder.spawn(( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 94f4fe809f..91918b1d66 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 7b1abfd8da..3123e8d891 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = JustifyText::Center; + let text_justification = Justify::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -78,7 +78,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::WordBoundary), + TextLayout::new(Justify::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -94,7 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::AnyCharacter), + TextLayout::new(Justify::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -104,11 +104,11 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate font smoothing off commands.spawn(( - Text2d::new("This text has\nFontSmoothing::None\nAnd JustifyText::Center"), + Text2d::new("This text has\nFontSmoothing::None\nAnd Justify::Center"), slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), )); diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 7510afbcee..25106adcfb 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index e29574588c..fd93625c0e 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -5,24 +5,25 @@ use std::{f32::consts::PI, fmt::Write}; use bevy::{ anti_aliasing::{ contrast_adaptive_sharpening::ContrastAdaptiveSharpening, - experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, fxaa::{Fxaa, Sensitivity}, smaa::{Smaa, SmaaPreset}, + taa::TemporalAntiAliasing, }, core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass}, image::{ImageSampler, ImageSamplerDescriptor}, pbr::CascadeShadowConfigBuilder, prelude::*, render::{ - camera::TemporalJitter, + camera::{MipBias, TemporalJitter}, render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, + view::Hdr, }, }; fn main() { App::new() - .add_plugins((DefaultPlugins, TemporalAntiAliasPlugin)) + .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (modify_aa, modify_sharpening, update_ui)) .run(); @@ -31,6 +32,7 @@ fn main() { type TaaComponents = ( TemporalAntiAliasing, TemporalJitter, + MipBias, DepthPrepass, MotionVectorPrepass, ); @@ -300,10 +302,7 @@ fn setup( // Camera commands.spawn(( Camera3d::default(), - Camera { - hdr: true, - ..default() - }, + Hdr, Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), ContrastAdaptiveSharpening { enabled: false, diff --git a/examples/3d/atmosphere.rs b/examples/3d/atmosphere.rs index 53c5c91dfa..edc6d04dab 100644 --- a/examples/3d/atmosphere.rs +++ b/examples/3d/atmosphere.rs @@ -20,11 +20,6 @@ fn main() { fn setup_camera_fog(mut commands: Commands) { commands.spawn(( Camera3d::default(), - // HDR is required for atmospheric scattering to be properly applied to the scene - Camera { - hdr: true, - ..default() - }, Transform::from_xyz(-1.2, 0.15, 0.0).looking_at(Vec3::Y * 0.1, Vec3::Y), // This is the component that enables atmospheric scattering for a camera Atmosphere::EARTH, @@ -36,7 +31,7 @@ fn setup_camera_fog(mut commands: Commands) { scene_units_to_m: 1e+4, ..Default::default() }, - // The directional light illuminance used in this scene + // The directional light illuminance used in this scene // (the one recommended for use with this feature) is // quite bright, so raising the exposure compensation helps // bring the scene to a nicer brightness range. diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 79fece61c8..62c875dc5d 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -40,10 +40,6 @@ fn setup( commands.spawn(( Camera3d::default(), - Camera { - hdr: true, - ..default() - }, Transform::from_xyz(1.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), AutoExposure { metering_mask: metering_mask.clone(), diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs index 830acfdb34..95fe522cf0 100644 --- a/examples/3d/blend_modes.rs +++ b/examples/3d/blend_modes.rs @@ -10,7 +10,7 @@ //! | `Spacebar` | Toggle Unlit | //! | `C` | Randomize Colors | -use bevy::{color::palettes::css::ORANGE, prelude::*}; +use bevy::{color::palettes::css::ORANGE, prelude::*, render::view::Hdr}; use rand::random; fn main() { @@ -149,6 +149,7 @@ fn setup( commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 2.5, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + Hdr, // Unfortunately, MSAA and HDR are not supported simultaneously under WebGL. // Since this example uses HDR, we must disable MSAA for Wasm builds, at least // until WebGPU is ready and no longer behind a feature flag in Web browsers. @@ -249,13 +250,23 @@ impl Default for ExampleState { fn example_control_system( mut materials: ResMut>, controllable: Query<(&MeshMaterial3d, &ExampleControls)>, - camera: Single<(&mut Camera, &mut Transform, &GlobalTransform), With>, + camera: Single< + ( + Entity, + &mut Camera, + &mut Transform, + &GlobalTransform, + Has, + ), + With, + >, mut labels: Query<(&mut Node, &ExampleLabel)>, mut display: Single<&mut Text, With>, labeled: Query<&GlobalTransform>, mut state: Local, time: Res, mut b: EventReade } /// A dummy event type. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] struct DebugEvent { resend_from_param_set: bool, resend_from_local_event_reader: bool, diff --git a/examples/ecs/state_scoped.rs b/examples/ecs/state_scoped.rs index e0844b119d..52a01c4d78 100644 --- a/examples/ecs/state_scoped.rs +++ b/examples/ecs/state_scoped.rs @@ -10,7 +10,6 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .init_state::() - .enable_state_scoped_entities::() .add_systems(Startup, setup_camera) .add_systems(OnEnter(GameState::A), on_a_enter) .add_systems(OnEnter(GameState::B), on_b_enter) @@ -117,7 +116,7 @@ fn toggle( state: Res>, mut next_state: ResMut>, ) { - if !timer.0.tick(time.delta()).finished() { + if !timer.0.tick(time.delta()).is_finished() { return; } *next_state = match state.get() { diff --git a/examples/games/alien_cake_addict.rs b/examples/games/alien_cake_addict.rs index 5051c390f8..d0aa9c8680 100644 --- a/examples/games/alien_cake_addict.rs +++ b/examples/games/alien_cake_addict.rs @@ -25,7 +25,6 @@ fn main() { TimerMode::Repeating, ))) .init_state::() - .enable_state_scoped_entities::() .add_systems(Startup, setup_cameras) .add_systems(OnEnter(GameState::Playing), setup) .add_systems( @@ -204,7 +203,7 @@ fn move_player( mut transforms: Query<&mut Transform>, time: Res