diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7da6ab768c1da..79f8623d81990 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: env: CARGO_TERM_COLOR: always - NIGHTLY_TOOLCHAIN: nightly + NIGHTLY_TOOLCHAIN: nightly-2024-01-17 jobs: build: @@ -276,7 +276,7 @@ jobs: - name: log failed task - missing update if: ${{ failure() && github.event_name == 'pull_request' && steps.missing-update.conclusion == 'failure' }} run: touch ./missing-examples/missing-update - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() && github.event_name == 'pull_request' }} with: name: missing-examples @@ -310,7 +310,7 @@ jobs: - name: log failed task - missing update if: ${{ failure() && github.event_name == 'pull_request' && steps.missing-update.conclusion == 'failure' }} run: touch ./missing-features/missing-update - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() && github.event_name == 'pull_request' }} with: name: missing-features @@ -348,7 +348,7 @@ jobs: run: | mkdir -p ./msrv echo ${{ github.event.number }} > ./msrv/NR - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() && github.event_name == 'pull_request' && steps.check.conclusion == 'failure' }} with: name: msrv diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 4fd4c37e8a69d..8c43341720a01 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -120,7 +120,7 @@ jobs: - name: Save screenshots if: ${{ always() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: screenshots-${{ matrix.device }}-${{ matrix.os_version }} path: .github/start-mobile-example/*.png diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index c8f11c16bf9d9..424f2354c7a36 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -29,36 +29,11 @@ jobs: check-bans: runs-on: ubuntu-latest steps: - # on main, prepare a new cargo tree output and cache it - - name: On main, prepare new cargo tree cache - if: github.ref == 'refs/heads/main' - run: cargo tree --depth 3 > cargo-tree-from-main - - name: On main, save the new cargo tree cache - if: github.ref == 'refs/heads/main' - uses: actions/cache/save@v3 - with: - path: cargo-tree-from-main - key: cargo-tree-from-main - # on other branch, restore the cached cargo tree output - - name: On PR, restore cargo tree cache - uses: actions/cache/restore@v3 - if: github.ref != 'refs/heads/main' - with: - path: cargo-tree-from-main - key: cargo-tree-from-main - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - # if not on main, check that the cargo tree output is unchanged - - name: Check if the cargo tree changed from main - if: github.ref != 'refs/heads/main' - continue-on-error: true - id: cargo-tree-changed - run: diff cargo-tree-from-main <(cargo tree --depth 3) - name: Install cargo-deny run: cargo install cargo-deny - # if the check was not a success (either skipped because on main or failed because of a change), run the check - name: Check for banned and duplicated dependencies - if: steps.cargo-tree-changed.outcome != 'success' run: cargo deny check bans check-licenses: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e66554022c2c4..f331e5f0247fa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,13 +5,30 @@ on: branches: - 'main' + # Allows running the action manually. + workflow_dispatch: + env: CARGO_TERM_COLOR: always RUSTDOCFLAGS: --html-in-header header.html +# Sets the permissions to allow deploying to Github pages. +permissions: + contents: read + pages: write + id-token: write + +# Only allow one deployment to run at a time, however it will not cancel in-progress runs. +concurrency: + group: "pages" + cancel-in-progress: false + jobs: build-and-deploy: runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout uses: actions/checkout@v4 @@ -37,19 +54,17 @@ jobs: # - A top level redirect to the bevy crate documentation # - A CNAME file for redirecting the docs domain to the API reference # - A robots.txt file to forbid any crawling of the site (to defer to the docs.rs site on search engines). - # - A .nojekyll file to disable Jekyll GitHub Pages builds. - name: Finalize documentation run: | echo "" > target/doc/index.html echo "dev-docs.bevyengine.org" > target/doc/CNAME echo "User-Agent: *\nDisallow: /" > target/doc/robots.txt - touch target/doc/.nojekyll - - name: Deploy - if: github.repository == 'bevyengine/bevy' - uses: JamesIves/github-pages-deploy-action@v4 + - name: Upload site artifact + uses: actions/upload-pages-artifact@v3 with: - branch: gh-pages - folder: target/doc - single-commit: true - force: true + path: target/doc + + - name: Deploy to Github Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index f48bf24254f14..288a175e7a7c5 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -70,11 +70,11 @@ jobs: run: | sudo apt-get update; DEBIAN_FRONTEND=noninteractive sudo apt-get install --no-install-recommends -yq \ - libasound2-dev libudev-dev; + libasound2-dev libudev-dev libxkbcommon-x11-0; - name: install xvfb, llvmpipe and lavapipe run: | sudo apt-get update -y -qq - sudo add-apt-repository ppa:kisak/kisak-mesa -y + sudo add-apt-repository ppa:kisak/turtle -y sudo apt-get update sudo apt install -y xvfb libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers - uses: actions/checkout@v4 @@ -108,16 +108,16 @@ jobs: zip traces.zip trace*.json zip -r screenshots.zip screenshots-* - name: save traces - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: example-traces.zip path: traces.zip - name: save screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: screenshots.zip path: screenshots.zip - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: ${{ failure() && github.event_name == 'pull_request' }} with: name: example-run @@ -148,7 +148,7 @@ jobs: - name: install xvfb, llvmpipe and lavapipe run: | sudo apt-get update -y -qq - sudo add-apt-repository ppa:kisak/kisak-mesa -y + sudo add-apt-repository ppa:kisak/turtle -y sudo apt-get update sudo apt install -y xvfb libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers @@ -175,7 +175,7 @@ jobs: xvfb-run cargo run -p build-wasm-example -- --browsers chromium --browsers firefox --frames 25 --test 2d_shapes lighting text_debug breakout - name: Save screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: screenshots path: .github/start-wasm-example/screenshot-*.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 712ca05c36a5e..9c7db8af6947d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,10 @@ It's a nice place to chat about Bevy development, ask questions, and get to know Read on if you're looking for: -* the high-level design goals of Bevy -* conventions and informal practices we follow when developing Bevy -* general advice on good open source collaboration practices -* concrete ways you can help us, no matter your background or skill level +* The high-level design goals of Bevy. +* Conventions and informal practices we follow when developing Bevy. +* General advice on good open source collaboration practices. +* Concrete ways you can help us, no matter your background or skill level. We're thrilled to have you along as we build! @@ -40,12 +40,12 @@ Some crates of interest: Bevy is a completely free and open source game engine built in Rust. It currently has the following design goals: -* **Capable**: Offer a complete 2D and 3D feature set -* **Simple**: Easy for newbies to pick up, but infinitely flexible for power users -* **Data Focused**: Data-oriented architecture using the Entity Component System paradigm -* **Modular**: Use only what you need. Replace what you don't like -* **Fast**: App logic should run quickly, and when possible, in parallel -* **Productive**: Changes should compile quickly ... waiting isn't fun +* **Capable**: Offer a complete 2D and 3D feature set. +* **Simple**: Easy for newbies to pick up, but infinitely flexible for power users. +* **Data Focused**: Data-oriented architecture using the Entity Component System paradigm. +* **Modular**: Use only what you need. Replace what you don't like. +* **Fast**: App logic should run quickly, and when possible, in parallel. +* **Productive**: Changes should compile quickly ... waiting isn't fun. Bevy also currently has the following "development process" goals: @@ -53,7 +53,7 @@ Bevy also currently has the following "development process" goals: * **Consistent vision**: The engine needs to feel consistent and cohesive. This takes precedence over democratic and/or decentralized processes. See our [*Bevy Organization doc*](/docs/the_bevy_organization.md) for more details. * **Flexibility over bureaucracy**: Developers should feel productive and unencumbered by development processes. * **Focus**: The Bevy Org should focus on building a small number of features excellently over merging every new community-contributed feature quickly. Sometimes this means pull requests will sit unmerged for a long time. This is the price of focus and we are willing to pay it. Fortunately Bevy is modular to its core. 3rd party plugins are a great way to work around this policy. -* **User-facing API ergonomics come first**: Solid user experience should receive significant focus and investment. It should rarely be compromised in the interest of internal implementation details. +* **User-facing API ergonomics come first**: Solid user experience should receive significant focus and investment. It should rarely be compromised in the interest of internal implementation details. * **Modularity over deep integration**: Individual crates and features should be "pluggable" whenever possible. Don't tie crates, features, or types together that don't need to be. * **Don't merge everything ... don't merge too early**: Every feature we add increases maintenance burden and compile times. Only merge features that are "generally" useful. Don't merge major changes or new features unless we have relative consensus that the design is correct *and* that we have the developer capacity to support it. When possible, make a 3rd party Plugin / crate first, then consider merging once the API has been tested in the wild. Bevy's modular structure means that the only difference between "official engine features" and "third party plugins" is our endorsement and the repo the code lives in. We should take advantage of that whenever possible. * **Control and consistency over 3rd party code reuse**: Only add a dependency if it is *absolutely* necessary. Every dependency we add decreases our autonomy and consistency. Dependencies also have the potential to increase compile times and risk pulling in sub-dependencies we don't want / need. @@ -78,17 +78,17 @@ Check out our dedicated [Bevy Organization document](/docs/the_bevy_organization Our merge strategy relies on the classification of PRs on two axes: -* How controversial are the design decisions -* How complex is the implementation +* How controversial are the design decisions. +* How complex is the implementation. Each [label](https://github.com/bevyengine/bevy/labels) has a prefix denoting its category: -* A: Area (e.g. A-Animation, A-ECS, A-Rendering) -* C: Category (e.g. C-Breaking-Change, C-Code-Quality, C-Docs) -* D: Difficulty (e.g. D-Complex, D-Good-First-Issue) -* O: Operating System (e.g. O-Linux, O-Web, O-Windows) -* P: Priority (e.g. P-Critical, P-High) -* S: Status (e.g. S-Blocked, S-Controversial, S-Needs-Design) +* A: Area (e.g. A-Animation, A-ECS, A-Rendering). +* C: Category (e.g. C-Breaking-Change, C-Code-Quality, C-Docs). +* D: Difficulty (e.g. D-Complex, D-Good-First-Issue). +* O: Operating System (e.g. O-Linux, O-Web, O-Windows). +* P: Priority (e.g. P-Critical, P-High). +* S: Status (e.g. S-Blocked, S-Controversial, S-Needs-Design). PRs with non-trivial design decisions are given the [`S-Controversial`] label. This indicates that the PR needs more thorough design review or an [RFC](https://github.com/bevyengine/rfcs), if complex enough. @@ -97,7 +97,7 @@ PRs that are non-trivial to review are given the [`D-Complex`] label. This indic should be reviewed more thoroughly and by people with experience in the area that the PR touches. When making PRs, try to split out more controversial changes from less controversial ones, in order to make your work easier to review and merge. -It is also a good idea to try and split out simple changes from more complex changes if it is not helpful for then to be reviewed together. +It is also a good idea to try and split out simple changes from more complex changes if it is not helpful for them to be reviewed together. Some things that are reason to apply the [`S-Controversial`] label to a PR: @@ -106,42 +106,42 @@ Some things that are reason to apply the [`S-Controversial`] label to a PR: 3. Serious tradeoffs were made. 4. Heavy user impact. 5. New ways for users to make mistakes (footguns). -6. Adding a dependency +6. Adding a dependency. 7. Touching licensing information (due to level of precision required). -8. Adding root-level files (due to the high level of visibility) +8. Adding root-level files (due to the high level of visibility). Some things that are reason to apply the [`D-Complex`] label to a PR: -1. Introduction or modification of soundness relevant code (for example `unsafe` code) +1. Introduction or modification of soundness relevant code (for example `unsafe` code). 2. High levels of technical complexity. -3. Large-scale code reorganization +3. Large-scale code reorganization. Examples of PRs that are not [`S-Controversial`] or [`D-Complex`]: * Fixing dead links. * Removing dead code or unused dependencies. * Typo and grammar fixes. -* [Add `Mut::reborrow`](https://github.com/bevyengine/bevy/pull/7114) -* [Add `Res::clone`](https://github.com/bevyengine/bevy/pull/4109) +* [Add `Mut::reborrow`](https://github.com/bevyengine/bevy/pull/7114). +* [Add `Res::clone`](https://github.com/bevyengine/bevy/pull/4109). Examples of PRs that are [`S-Controversial`] but not [`D-Complex`]: -* [Implement and require `#[derive(Component)]` on all component structs](https://github.com/bevyengine/bevy/pull/2254) -* [Use default serde impls for Entity](https://github.com/bevyengine/bevy/pull/6194) +* [Implement and require `#[derive(Component)]` on all component structs](https://github.com/bevyengine/bevy/pull/2254). +* [Use default serde impls for Entity](https://github.com/bevyengine/bevy/pull/6194). Examples of PRs that are not [`S-Controversial`] but are [`D-Complex`]: -* [Ensure `Ptr`/`PtrMut`/`OwningPtr` are aligned in debug builds](https://github.com/bevyengine/bevy/pull/7117) -* [Replace `BlobVec`'s `swap_scratch` with a `swap_nonoverlapping`](https://github.com/bevyengine/bevy/pull/4853) +* [Ensure `Ptr`/`PtrMut`/`OwningPtr` are aligned in debug builds](https://github.com/bevyengine/bevy/pull/7117). +* [Replace `BlobVec`'s `swap_scratch` with a `swap_nonoverlapping`](https://github.com/bevyengine/bevy/pull/4853). Examples of PRs that are both [`S-Controversial`] and [`D-Complex`]: -* [bevy_reflect: Binary formats](https://github.com/bevyengine/bevy/pull/6140) +* [bevy_reflect: Binary formats](https://github.com/bevyengine/bevy/pull/6140). Some useful pull request queries: -* [PRs which need reviews and are not `D-Complex`](https://github.com/bevyengine/bevy/pulls?q=is%3Apr+-label%3AD-Complex+-label%3AS-Ready-For-Final-Review+-label%3AS-Blocked++) -* [`D-Complex` PRs which need reviews](https://github.com/bevyengine/bevy/pulls?q=is%3Apr+label%3AD-Complex+-label%3AS-Ready-For-Final-Review+-label%3AS-Blocked) +* [PRs which need reviews and are not `D-Complex`](https://github.com/bevyengine/bevy/pulls?q=is%3Apr+-label%3AD-Complex+-label%3AS-Ready-For-Final-Review+-label%3AS-Blocked++). +* [`D-Complex` PRs which need reviews](https://github.com/bevyengine/bevy/pulls?q=is%3Apr+label%3AD-Complex+-label%3AS-Ready-For-Final-Review+-label%3AS-Blocked). [`S-Controversial`]: https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+label%3AS-Controversial [`D-Complex`]: https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+label%3AD-Complex @@ -220,7 +220,7 @@ You can improve Bevy's ecosystem by building your own Bevy Plugins and crates. Non-trivial, reusable functionality that works well with itself is a good candidate for a plugin. If it's closer to a snippet or design pattern, you may want to share it with the community on [Discord], Reddit, or [GitHub Discussions] instead. -Check out our [plugin guidelines](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md) for helpful tips and patterns. +Check out our [plugin guidelines](https://bevyengine.org/learn/book/plugin-development/) for helpful tips and patterns. ### Fixing bugs @@ -253,23 +253,22 @@ which has the latest API reference built from the repo on every commit made to t ### Writing examples -Most [examples in Bevy](https://github.com/bevyengine/bevy/tree/main/examples) aim to clearly demonstrate a single feature, group of closely related small features, or show how to accomplish a particular task (such as asset loading, creating a custom shader or testing your app). -In rare cases, creating new "game" examples is justified in order to demonstrate new features -that open a complex class of functionality in a way that's hard to demonstrate in isolation or requires additional integration testing. +Most [examples in Bevy](https://github.com/bevyengine/bevy/tree/main/examples) aim to clearly demonstrate a single feature, group of closely related small features, or show how to accomplish a particular task (such as asset loading, creating a custom shader or testing your app). +In rare cases, creating new "game" examples is justified in order to demonstrate new features that open a complex class of functionality in a way that's hard to demonstrate in isolation or requires additional integration testing. -Examples in Bevy should be: +Examples in Bevy should be: -1. **Working:** They must compile and run, and any introduced errors in them should be obvious (through tests, simple results or clearly displayed behavior). -2. **Clear:** They must use descriptive variable names, be formatted, and be appropriately commented. Try your best to showcase best practices when it doesn't obscure the point of the example. -3. **Relevant:** They should explain, through comments or variable names, what they do and how this can be useful to a game developer. -4. **Minimal:** They should be no larger or complex than is needed to meet the goals of the example. +1. **Working:** They must compile and run, and any introduced errors in them should be obvious (through tests, simple results or clearly displayed behavior). +2. **Clear:** They must use descriptive variable names, be formatted, and be appropriately commented. Try your best to showcase best practices when it doesn't obscure the point of the example. +3. **Relevant:** They should explain, through comments or variable names, what they do and how this can be useful to a game developer. +4. **Minimal:** They should be no larger or complex than is needed to meet the goals of the example. When you add a new example, be sure to update `examples/README.md` with the new example and add it to the root `Cargo.toml` file. Run `cargo run -p build-templated-pages -- build-example-page` to do this automatically. Use a generous sprinkling of keywords in your description: these are commonly used to search for a specific example. See the [example style guide](.github/contributing/example_style_guide.md) to help make sure the style of your example matches what we're already using. -More complex demonstrations of functionality are also welcome, but these should be submitted to [bevy-assets](https://github.com/bevyengine/bevy-assets). +More complex demonstrations of functionality are also welcome, but these should be submitted to [bevy-assets](https://github.com/bevyengine/bevy-assets). ### Reviewing others' work @@ -289,7 +288,7 @@ If you're new to GitHub, check out the [Pull Request Review documentation](https There are three main places you can check for things to review: -1. Pull request which are ready and in need of more reviews on [bevy](https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+-label%3AS-Ready-For-Final-Review+-draft%3A%3Atrue+-label%3AS-Needs-RFC+-reviewed-by%3A%40me+-author%3A%40me) +1. Pull requests which are ready and in need of more reviews on [bevy](https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+-label%3AS-Ready-For-Final-Review+-draft%3A%3Atrue+-label%3AS-Needs-RFC+-reviewed-by%3A%40me+-author%3A%40me). 2. Pull requests on [bevy](https://github.com/bevyengine/bevy/pulls) and the [bevy-website](https://github.com/bevyengine/bevy-website/pulls) repos. 3. [RFCs](https://github.com/bevyengine/rfcs), which need extensive thoughtful community input on their design. @@ -329,11 +328,11 @@ If you're new to Bevy, here's the workflow we use: 1. Try to split your work into separate commits, each with a distinct purpose. Be particularly mindful of this when responding to reviews so it's easy to see what's changed. 2. Tip: [You can set up a global `.gitignore` file](https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files#configuring-ignored-files-for-all-repositories-on-your-computer) to exclude your operating system/text editor's special/temporary files. (e.g. `.DS_Store`, `thumbs.db`, `*~`, `*.swp` or `*.swo`) This allows us to keep the `.gitignore` file in the repo uncluttered. 3. To test CI validations locally, run the `cargo run -p ci` command. This will run most checks that happen in CI, but can take some time. You can also run sub-commands to iterate faster depending on what you're contributing: - * `cargo run -p ci -- lints` - to run formatting and clippy - * `cargo run -p ci -- test` - to run tests - * `cargo run -p ci -- doc` - to run doc tests and doc checks - * `cargo run -p ci -- compile` - to check that everything that must compile still does (examples and benches), and that some that shouldn't still don't ([`crates/bevy_ecs_compile_fail_tests`](./crates/bevy_ecs_compile_fail_tests)) - * to get more information on commands available and what is run, check the [tools/ci crate](./tools/ci) + * `cargo run -p ci -- lints` - to run formatting and clippy. + * `cargo run -p ci -- test` - to run tests. + * `cargo run -p ci -- doc` - to run doc tests and doc checks. + * `cargo run -p ci -- compile` - to check that everything that must compile still does (examples and benches), and that some that shouldn't still don't ([`crates/bevy_ecs_compile_fail_tests`](./crates/bevy_ecs_compile_fail_tests)). + * to get more information on commands available and what is run, check the [tools/ci crate](./tools/ci). 4. When working with Markdown (`.md`) files, Bevy's CI will check markdown files (like this one) using [markdownlint](https://github.com/DavidAnson/markdownlint). To locally lint your files using the same workflow as our CI: diff --git a/Cargo.toml b/Cargo.toml index 0121c1646c75f..65bf72603684f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -455,6 +455,26 @@ description = "Renders an animated sprite" category = "2D Rendering" wasm = true +[[example]] +name = "sprite_tile" +path = "examples/2d/sprite_tile.rs" + +[package.metadata.example.sprite_tile] +name = "Sprite Tile" +description = "Renders a sprite tiled in a grid" +category = "2D Rendering" +wasm = true + +[[example]] +name = "sprite_slice" +path = "examples/2d/sprite_slice.rs" + +[package.metadata.example.sprite_slice] +name = "Sprite Slice" +description = "Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique" +category = "2D Rendering" +wasm = true + [[example]] name = "text2d" path = "examples/2d/text2d.rs" @@ -489,13 +509,12 @@ category = "2D Rendering" wasm = true [[example]] -name = "pixel_perfect" -path = "examples/2d/pixel_perfect.rs" -doc-scrape-examples = true +name = "pixel_grid_snap" +path = "examples/2d/pixel_grid_snap.rs" -[package.metadata.example.pixel_perfect] -name = "Pixel Perfect" -description = "Demonstrates pixel perfect in 2d" +[package.metadata.example.pixel_grid_snap] +name = "Pixel Grid Snapping" +description = "Shows how to create graphics that snap to the pixel grid by rendering to a texture in 2D" category = "2D Rendering" wasm = true @@ -599,6 +618,17 @@ description = "Showcases different blend modes" category = "3D Rendering" wasm = true +[[example]] +name = "deterministic" +path = "examples/3d/deterministic.rs" +doc-scrape-examples = true + +[package.metadata.example.deterministic] +name = "Deterministic rendering" +description = "Stop flickering from z-fighting at a performance cost" +category = "3D Rendering" +wasm = true + [[example]] name = "lighting" path = "examples/3d/lighting.rs" @@ -872,6 +902,17 @@ description = "Showcases wireframe rendering" category = "3D Rendering" wasm = false +[[example]] +name = "lightmaps" +path = "examples/3d/lightmaps.rs" +doc-scrape-examples = true + +[package.metadata.example.lightmaps] +name = "Lightmaps" +description = "Rendering a scene with baked lightmaps" +category = "3D Rendering" +wasm = false + [[example]] name = "no_prepass" path = "tests/3d/no_prepass.rs" @@ -1014,6 +1055,16 @@ description = "Illustrate how to use generate log output" category = "Application" wasm = true +[[example]] +name = "log_layers" +path = "examples/app/log_layers.rs" + +[package.metadata.example.log_layers] +name = "Log layers" +description = "Illustrate how to add custom log layers" +category = "Application" +wasm = false + [[example]] name = "plugin" path = "examples/app/plugin.rs" @@ -1273,11 +1324,6 @@ description = "Full guide to Bevy's ECS" category = "ECS (Entity Component System)" wasm = false -[[example]] -name = "apply_deferred" -path = "examples/ecs/apply_deferred.rs" -doc-scrape-examples = true - [package.metadata.example.apply_deferred] name = "Apply System Buffers" description = "Show how to use `apply_deferred` system" @@ -1306,6 +1352,17 @@ description = "Groups commonly used compound queries and query filters into a si category = "ECS (Entity Component System)" wasm = false +[[example]] +name = "dynamic" +path = "examples/ecs/dynamic.rs" +doc-scrape-examples = true + +[package.metadata.example.dynamic] +name = "Dynamic ECS" +description = "Dynamically create components, spawn entities with those components and query those components" +category = "ECS (Entity Component System)" +wasm = false + [[example]] name = "event" path = "examples/ecs/event.rs" @@ -1840,6 +1897,7 @@ wasm = true name = "shader_material_glsl" path = "examples/shader/shader_material_glsl.rs" doc-scrape-examples = true +required-features = ["shader_format_glsl"] [package.metadata.example.shader_material_glsl] name = "Material - GLSL" @@ -2203,6 +2261,17 @@ description = "Showcases the RelativeCursorPosition component" category = "UI (User Interface)" wasm = true +[[example]] +name = "render_ui_to_texture" +path = "examples/ui/render_ui_to_texture.rs" +doc-scrape-examples = true + +[package.metadata.example.render_ui_to_texture] +name = "Render UI to Texture" +description = "An example of rendering UI as a part of a 3D world" +category = "UI (User Interface)" +wasm = true + [[example]] name = "size_constraints" path = "examples/ui/size_constraints.rs" @@ -2440,6 +2509,17 @@ name = "fallback_image" path = "examples/shader/fallback_image.rs" doc-scrape-examples = true +[[example]] +name = "reflection_probes" +path = "examples/3d/reflection_probes.rs" +doc-scrape-examples = true + +[package.metadata.example.reflection_probes] +name = "Reflection Probes" +description = "Demonstrates reflection probes" +category = "3D Rendering" +wasm = false + [package.metadata.example.fallback_image] hidden = true diff --git a/README.md b/README.md index 9a7a498cac5ad..81309e8c7e4c8 100644 --- a/README.md +++ b/README.md @@ -92,12 +92,6 @@ This [list][cargo_features] outlines the different cargo features supported by B [cargo_features]: docs/cargo_features.md -## [Third Party Plugins][plugin_guidelines] - -Plugins are very welcome to extend Bevy's features. [Guidelines][plugin_guidelines] are available to help integration and usage. - -[plugin_guidelines]: docs/plugins_guidelines.md - ## Thanks 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. diff --git a/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 b/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 new file mode 100644 index 0000000000000..9c2f2a85a3bb8 Binary files /dev/null and b/assets/environment_maps/cubes_reflection_probe_specular_rgb9e5_zstd.ktx2 differ diff --git a/assets/lightmaps/CornellBox-Box.zstd.ktx2 b/assets/lightmaps/CornellBox-Box.zstd.ktx2 new file mode 100644 index 0000000000000..0a31dd48541c7 Binary files /dev/null and b/assets/lightmaps/CornellBox-Box.zstd.ktx2 differ diff --git a/assets/lightmaps/CornellBox-Large.zstd.ktx2 b/assets/lightmaps/CornellBox-Large.zstd.ktx2 new file mode 100644 index 0000000000000..c41ef9b032692 Binary files /dev/null and b/assets/lightmaps/CornellBox-Large.zstd.ktx2 differ diff --git a/assets/lightmaps/CornellBox-Small.zstd.ktx2 b/assets/lightmaps/CornellBox-Small.zstd.ktx2 new file mode 100644 index 0000000000000..9b1b72cd13935 Binary files /dev/null and b/assets/lightmaps/CornellBox-Small.zstd.ktx2 differ diff --git a/assets/models/CornellBox/CornellBox.glb b/assets/models/CornellBox/CornellBox.glb new file mode 100644 index 0000000000000..9cb6661be36ee Binary files /dev/null and b/assets/models/CornellBox/CornellBox.glb differ diff --git a/assets/models/cubes/Cubes.glb b/assets/models/cubes/Cubes.glb new file mode 100644 index 0000000000000..9e1b481ebfe0e Binary files /dev/null and b/assets/models/cubes/Cubes.glb differ diff --git a/assets/pixel/bevy_pixel_dark.png b/assets/pixel/bevy_pixel_dark.png index 563531d0a5c95..93fe567b34bed 100644 Binary files a/assets/pixel/bevy_pixel_dark.png and b/assets/pixel/bevy_pixel_dark.png differ diff --git a/assets/pixel/bevy_pixel_light.png b/assets/pixel/bevy_pixel_light.png index f6225fe25eabb..03edd462457cf 100644 Binary files a/assets/pixel/bevy_pixel_light.png and b/assets/pixel/bevy_pixel_light.png differ diff --git a/assets/textures/slice_square.png b/assets/textures/slice_square.png new file mode 100644 index 0000000000000..bee873c46b5a4 Binary files /dev/null and b/assets/textures/slice_square.png differ diff --git a/assets/textures/slice_square_2.png b/assets/textures/slice_square_2.png new file mode 100644 index 0000000000000..b38d6ee6664be Binary files /dev/null and b/assets/textures/slice_square_2.png differ diff --git a/benches/Cargo.toml b/benches/Cargo.toml index b78352a55dabe..7afdde7e2001e 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -7,7 +7,7 @@ publish = false license = "MIT OR Apache-2.0" [dev-dependencies] -glam = "0.24.1" +glam = "0.25" rand = "0.8" rand_chacha = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/benches/benches/bevy_utils/entity_hash.rs b/benches/benches/bevy_utils/entity_hash.rs index b9546fd27ac2c..44cf80ba9a4ec 100644 --- a/benches/benches/bevy_utils/entity_hash.rs +++ b/benches/benches/bevy_utils/entity_hash.rs @@ -13,12 +13,12 @@ fn make_entity(rng: &mut impl Rng, size: usize) -> Entity { // -log₂(1-x) gives an exponential distribution with median 1.0 // That lets us get values that are mostly small, but some are quite large // * For ids, half are in [0, size), half are unboundedly larger. - // * For generations, half are in [0, 2), half are unboundedly larger. + // * For generations, half are in [1, 3), half are unboundedly larger. let x: f64 = rng.gen(); let id = -(1.0 - x).log2() * (size as f64); let x: f64 = rng.gen(); - let gen = -(1.0 - x).log2() * 2.0; + let gen = 1.0 + -(1.0 - x).log2() * 2.0; // this is not reliable, but we're internal so a hack is ok let bits = ((gen as u64) << 32) | (id as u64); diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 4a1948c065ec7..44e4f5cb9031a 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -105,6 +105,7 @@ impl Plugin for AccessibilityPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::() .init_resource::() - .init_resource::(); + .init_resource::() + .allow_ambiguous_component::(); } } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index fc5d1d2804e66..02f8139a697d5 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -10,7 +10,7 @@ use bevy_asset::{Asset, AssetApp, Assets, Handle}; use bevy_core::Name; use bevy_ecs::prelude::*; use bevy_hierarchy::{Children, Parent}; -use bevy_math::{Quat, Vec3}; +use bevy_math::{FloatExt, Quat, Vec3}; use bevy_reflect::Reflect; use bevy_render::mesh::morph::MorphWeights; use bevy_time::Time; @@ -21,7 +21,8 @@ use bevy_utils::{tracing::warn, HashMap}; pub mod prelude { #[doc(hidden)] pub use crate::{ - AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Keyframes, VariableCurve, + AnimationClip, AnimationPlayer, AnimationPlugin, EntityPath, Interpolation, Keyframes, + VariableCurve, }; } @@ -45,6 +46,22 @@ pub enum Keyframes { Weights(Vec), } +impl Keyframes { + /// Returns the number of keyframes. + pub fn len(&self) -> usize { + match self { + Keyframes::Weights(vec) => vec.len(), + Keyframes::Translation(vec) | Keyframes::Scale(vec) => vec.len(), + Keyframes::Rotation(vec) => vec.len(), + } + } + + /// Returns true if the number of keyframes is zero. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + /// Describes how an attribute of a [`Transform`] or [`MorphWeights`] should be animated. /// /// `keyframe_timestamps` and `keyframes` should have the same length. @@ -53,7 +70,71 @@ pub struct VariableCurve { /// Timestamp for each of the keyframes. pub keyframe_timestamps: Vec, /// List of the keyframes. + /// + /// The representation will depend on the interpolation type of this curve: + /// + /// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value + /// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`, + /// `keyframe_value` and `tangent_out` pub keyframes: Keyframes, + /// Interpolation method to use between keyframes. + pub interpolation: Interpolation, +} + +impl VariableCurve { + /// Find the index of the keyframe at or before the current time. + /// + /// Returns [`None`] if the curve is finished or not yet started. + /// To be more precise, this returns [`None`] if the frame is at or past the last keyframe: + /// we cannot get the *next* keyframe to interpolate to in that case. + pub fn find_current_keyframe(&self, seek_time: f32) -> Option { + // An Ok(keyframe_index) result means an exact result was found by binary search + // An Err result means the keyframe was not found, and the index is the keyframe + // PERF: finding the current keyframe can be optimised + let search_result = self + .keyframe_timestamps + .binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap()); + + // Subtract one for zero indexing! + let last_keyframe = self.keyframes.len() - 1; + + // We want to find the index of the keyframe before the current time + // If the keyframe is past the second-to-last keyframe, the animation cannot be interpolated. + let step_start = match search_result { + // An exact match was found, and it is the last keyframe (or something has gone terribly wrong). + // This means that the curve is finished. + Ok(n) if n >= last_keyframe => return None, + // An exact match was found, and it is not the last keyframe. + Ok(i) => i, + // No exact match was found, and the seek_time is before the start of the animation. + // This occurs because the binary search returns the index of where we could insert a value + // without disrupting the order of the vector. + // If the value is less than the first element, the index will be 0. + Err(0) => return None, + // No exact match was found, and it was after the last keyframe. + // The curve is finished. + Err(n) if n > last_keyframe => return None, + // No exact match was found, so return the previous keyframe to interpolate from. + Err(i) => i - 1, + }; + + // Consumers need to be able to interpolate between the return keyframe and the next + assert!(step_start < self.keyframe_timestamps.len()); + + Some(step_start) + } +} + +/// Interpolation method to use between keyframes. +#[derive(Reflect, Clone, Debug)] +pub enum Interpolation { + /// Linear interpolation between the two closest keyframes. + Linear, + /// Step interpolation, the value of the start keyframe is used. + Step, + /// Cubic spline interpolation. The value of the two closest keyframes is used, with the out + /// tangent of the start keyframe and the in tangent of the end keyframe. + CubicSpline, } /// Path to an entity, with [`Name`]s. Each entity in a path must have a name. @@ -572,7 +653,7 @@ fn run_animation_player( fn lerp_morph_weights(weights: &mut [f32], keyframe: impl Iterator, key_lerp: f32) { let zipped = weights.iter_mut().zip(keyframe); for (morph_weight, keyframe) in zipped { - *morph_weight += (keyframe - *morph_weight) * key_lerp; + *morph_weight = morph_weight.lerp(keyframe, key_lerp); } } @@ -591,6 +672,18 @@ fn get_keyframe(target_count: usize, keyframes: &[f32], key_index: usize) -> &[f &keyframes[start..end] } +// Helper macro for cubic spline interpolation +// it needs to work on `f32`, `Vec3` and `Quat` +// TODO: replace by a function if the proper trait bounds can be figured out +macro_rules! cubic_spline_interpolation { + ($value_start: expr, $tangent_out_start: expr, $tangent_in_end: expr, $value_end: expr, $lerp: expr, $step_duration: expr,) => { + $value_start * (2.0 * $lerp.powi(3) - 3.0 * $lerp.powi(2) + 1.0) + + $tangent_out_start * ($step_duration) * ($lerp.powi(3) - 2.0 * $lerp.powi(2) + $lerp) + + $value_end * (-2.0 * $lerp.powi(3) + 3.0 * $lerp.powi(2)) + + $tangent_in_end * ($step_duration) * ($lerp.powi(3) - $lerp.powi(2)) + }; +} + #[allow(clippy::too_many_arguments)] fn apply_animation( weight: f32, @@ -606,133 +699,238 @@ fn apply_animation( parents: &Query<(Has, Option<&Parent>)>, children: &Query<&Children>, ) { - if let Some(animation_clip) = animations.get(&animation.animation_clip) { - // We don't return early because seek_to() may have been called on the animation player. - animation.update( - if paused { 0.0 } else { time.delta_seconds() }, - animation_clip.duration, - ); + let Some(animation_clip) = animations.get(&animation.animation_clip) else { + return; + }; - if animation.path_cache.len() != animation_clip.paths.len() { - animation.path_cache = vec![Vec::new(); animation_clip.paths.len()]; - } - if !verify_no_ancestor_player(maybe_parent, parents) { - warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root); - return; - } + // We don't return early because seek_to() may have been called on the animation player. + animation.update( + if paused { 0.0 } else { time.delta_seconds() }, + animation_clip.duration, + ); - let mut any_path_found = false; - for (path, bone_id) in &animation_clip.paths { - let cached_path = &mut animation.path_cache[*bone_id]; - let curves = animation_clip.get_curves(*bone_id).unwrap(); - let Some(target) = entity_from_path(root, path, children, names, cached_path) else { - continue; - }; - any_path_found = true; - // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias - // any of their descendant Transforms. - // - // The system scheduler prevents any other system from mutating Transforms at the same time, - // so the only way this fetch can alias is if two AnimationPlayers are targeting the same bone. - // This can only happen if there are two or more AnimationPlayers are ancestors to the same - // entities. By verifying that there is no other AnimationPlayer in the ancestors of a - // running AnimationPlayer before animating any entity, this fetch cannot alias. - // - // This means only the AnimationPlayers closest to the root of the hierarchy will be able - // to run their animation. Any players in the children or descendants will log a warning - // and do nothing. - let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else { - continue; - }; - // SAFETY: As above, there can't be other AnimationPlayers with this target so this fetch can't alias - let mut morphs = unsafe { morphs.get_unchecked(target) }; - for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - match &curve.keyframes { - Keyframes::Rotation(keyframes) => { - transform.rotation = transform.rotation.slerp(keyframes[0], weight); - } - Keyframes::Translation(keyframes) => { - transform.translation = - transform.translation.lerp(keyframes[0], weight); - } - Keyframes::Scale(keyframes) => { - transform.scale = transform.scale.lerp(keyframes[0], weight); - } - Keyframes::Weights(keyframes) => { - if let Ok(morphs) = &mut morphs { - let target_count = morphs.weights().len(); - lerp_morph_weights( - morphs.weights_mut(), - get_keyframe(target_count, keyframes, 0).iter().copied(), - weight, - ); - } - } - } - continue; - } + if animation.path_cache.len() != animation_clip.paths.len() { + let new_len = animation_clip.paths.len(); + animation.path_cache.iter_mut().for_each(|v| v.clear()); + animation.path_cache.resize_with(new_len, Vec::new); + } + if !verify_no_ancestor_player(maybe_parent, parents) { + warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root); + return; + } - // Find the current keyframe - // PERF: finding the current keyframe can be optimised - let step_start = match curve - .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&animation.seek_time).unwrap()) - { - Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished - Ok(i) => i, - Err(0) => continue, // this curve isn't started yet - Err(n) if n > curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished - Err(i) => i - 1, - }; - let ts_start = curve.keyframe_timestamps[step_start]; - let ts_end = curve.keyframe_timestamps[step_start + 1]; - let lerp = (animation.seek_time - ts_start) / (ts_end - ts_start); - - // Apply the keyframe + let mut any_path_found = false; + for (path, bone_id) in &animation_clip.paths { + let cached_path = &mut animation.path_cache[*bone_id]; + let curves = animation_clip.get_curves(*bone_id).unwrap(); + let Some(target) = entity_from_path(root, path, children, names, cached_path) else { + continue; + }; + any_path_found = true; + // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias + // any of their descendant Transforms. + // + // The system scheduler prevents any other system from mutating Transforms at the same time, + // so the only way this fetch can alias is if two AnimationPlayers are targeting the same bone. + // This can only happen if there are two or more AnimationPlayers are ancestors to the same + // entities. By verifying that there is no other AnimationPlayer in the ancestors of a + // running AnimationPlayer before animating any entity, this fetch cannot alias. + // + // This means only the AnimationPlayers closest to the root of the hierarchy will be able + // to run their animation. Any players in the children or descendants will log a warning + // and do nothing. + let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else { + continue; + }; + // SAFETY: As above, there can't be other AnimationPlayers with this target so this fetch can't alias + let mut morphs = unsafe { morphs.get_unchecked(target) }.ok(); + for curve in curves { + // Some curves have only one keyframe used to set a transform + if curve.keyframe_timestamps.len() == 1 { match &curve.keyframes { Keyframes::Rotation(keyframes) => { - let rot_start = keyframes[step_start]; - let mut rot_end = keyframes[step_start + 1]; - // Choose the smallest angle for the rotation - if rot_end.dot(rot_start) < 0.0 { - rot_end = -rot_end; - } - // Rotations are using a spherical linear interpolation - let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp); - transform.rotation = transform.rotation.slerp(rot, weight); + transform.rotation = transform.rotation.slerp(keyframes[0], weight); } Keyframes::Translation(keyframes) => { - let translation_start = keyframes[step_start]; - let translation_end = keyframes[step_start + 1]; - let result = translation_start.lerp(translation_end, lerp); - transform.translation = transform.translation.lerp(result, weight); + transform.translation = transform.translation.lerp(keyframes[0], weight); } Keyframes::Scale(keyframes) => { - let scale_start = keyframes[step_start]; - let scale_end = keyframes[step_start + 1]; - let result = scale_start.lerp(scale_end, lerp); - transform.scale = transform.scale.lerp(result, weight); + transform.scale = transform.scale.lerp(keyframes[0], weight); } Keyframes::Weights(keyframes) => { - if let Ok(morphs) = &mut morphs { + if let Some(morphs) = &mut morphs { let target_count = morphs.weights().len(); - let morph_start = get_keyframe(target_count, keyframes, step_start); - let morph_end = get_keyframe(target_count, keyframes, step_start + 1); - let result = morph_start - .iter() - .zip(morph_end) - .map(|(a, b)| *a + lerp * (*b - *a)); - lerp_morph_weights(morphs.weights_mut(), result, weight); + lerp_morph_weights( + morphs.weights_mut(), + get_keyframe(target_count, keyframes, 0).iter().copied(), + weight, + ); } } } + continue; } + + // Find the current keyframe + let Some(step_start) = curve.find_current_keyframe(animation.seek_time) else { + continue; + }; + + let timestamp_start = curve.keyframe_timestamps[step_start]; + let timestamp_end = curve.keyframe_timestamps[step_start + 1]; + // Compute how far we are through the keyframe, normalized to [0, 1] + let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, animation.seek_time); + + apply_keyframe( + curve, + step_start, + weight, + lerp, + timestamp_end - timestamp_start, + &mut transform, + &mut morphs, + ); } + } - if !any_path_found { - warn!("Animation player on {root:?} did not match any entity paths."); + if !any_path_found { + warn!("Animation player on {root:?} did not match any entity paths."); + } +} + +#[inline(always)] +fn apply_keyframe( + curve: &VariableCurve, + step_start: usize, + weight: f32, + lerp: f32, + duration: f32, + transform: &mut Mut, + morphs: &mut Option>, +) { + match (&curve.interpolation, &curve.keyframes) { + (Interpolation::Step, Keyframes::Rotation(keyframes)) => { + transform.rotation = transform.rotation.slerp(keyframes[step_start], weight); + } + (Interpolation::Linear, Keyframes::Rotation(keyframes)) => { + let rot_start = keyframes[step_start]; + let mut rot_end = keyframes[step_start + 1]; + // Choose the smallest angle for the rotation + if rot_end.dot(rot_start) < 0.0 { + rot_end = -rot_end; + } + // Rotations are using a spherical linear interpolation + let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp); + transform.rotation = transform.rotation.slerp(rot, weight); + } + (Interpolation::CubicSpline, Keyframes::Rotation(keyframes)) => { + let value_start = keyframes[step_start * 3 + 1]; + let tangent_out_start = keyframes[step_start * 3 + 2]; + let tangent_in_end = keyframes[(step_start + 1) * 3]; + let value_end = keyframes[(step_start + 1) * 3 + 1]; + let result = cubic_spline_interpolation!( + value_start, + tangent_out_start, + tangent_in_end, + value_end, + lerp, + duration, + ); + transform.rotation = transform.rotation.slerp(result.normalize(), weight); + } + (Interpolation::Step, Keyframes::Translation(keyframes)) => { + transform.translation = transform.translation.lerp(keyframes[step_start], weight); + } + (Interpolation::Linear, Keyframes::Translation(keyframes)) => { + let translation_start = keyframes[step_start]; + let translation_end = keyframes[step_start + 1]; + let result = translation_start.lerp(translation_end, lerp); + transform.translation = transform.translation.lerp(result, weight); + } + (Interpolation::CubicSpline, Keyframes::Translation(keyframes)) => { + let value_start = keyframes[step_start * 3 + 1]; + let tangent_out_start = keyframes[step_start * 3 + 2]; + let tangent_in_end = keyframes[(step_start + 1) * 3]; + let value_end = keyframes[(step_start + 1) * 3 + 1]; + let result = cubic_spline_interpolation!( + value_start, + tangent_out_start, + tangent_in_end, + value_end, + lerp, + duration, + ); + transform.translation = transform.translation.lerp(result, weight); + } + (Interpolation::Step, Keyframes::Scale(keyframes)) => { + transform.scale = transform.scale.lerp(keyframes[step_start], weight); + } + (Interpolation::Linear, Keyframes::Scale(keyframes)) => { + let scale_start = keyframes[step_start]; + let scale_end = keyframes[step_start + 1]; + let result = scale_start.lerp(scale_end, lerp); + transform.scale = transform.scale.lerp(result, weight); + } + (Interpolation::CubicSpline, Keyframes::Scale(keyframes)) => { + let value_start = keyframes[step_start * 3 + 1]; + let tangent_out_start = keyframes[step_start * 3 + 2]; + let tangent_in_end = keyframes[(step_start + 1) * 3]; + let value_end = keyframes[(step_start + 1) * 3 + 1]; + let result = cubic_spline_interpolation!( + value_start, + tangent_out_start, + tangent_in_end, + value_end, + lerp, + duration, + ); + transform.scale = transform.scale.lerp(result, weight); + } + (Interpolation::Step, Keyframes::Weights(keyframes)) => { + if let Some(morphs) = morphs { + let target_count = morphs.weights().len(); + let morph_start = get_keyframe(target_count, keyframes, step_start); + lerp_morph_weights(morphs.weights_mut(), morph_start.iter().copied(), weight); + } + } + (Interpolation::Linear, Keyframes::Weights(keyframes)) => { + if let Some(morphs) = morphs { + let target_count = morphs.weights().len(); + let morph_start = get_keyframe(target_count, keyframes, step_start); + let morph_end = get_keyframe(target_count, keyframes, step_start + 1); + let result = morph_start + .iter() + .zip(morph_end) + .map(|(a, b)| a.lerp(*b, lerp)); + lerp_morph_weights(morphs.weights_mut(), result, weight); + } + } + (Interpolation::CubicSpline, Keyframes::Weights(keyframes)) => { + if let Some(morphs) = morphs { + let target_count = morphs.weights().len(); + let morph_start = get_keyframe(target_count, keyframes, step_start * 3 + 1); + let tangents_out_start = get_keyframe(target_count, keyframes, step_start * 3 + 2); + let tangents_in_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3); + let morph_end = get_keyframe(target_count, keyframes, (step_start + 1) * 3 + 1); + let result = morph_start + .iter() + .zip(tangents_out_start) + .zip(tangents_in_end) + .zip(morph_end) + .map( + |(((value_start, tangent_out_start), tangent_in_end), value_end)| { + cubic_spline_interpolation!( + value_start, + tangent_out_start, + tangent_in_end, + value_end, + lerp, + duration, + ) + }, + ); + lerp_morph_weights(morphs.weights_mut(), result, weight); + } } } } @@ -759,3 +957,152 @@ impl Plugin for AnimationPlugin { ); } } + +#[cfg(test)] +mod tests { + use crate::VariableCurve; + use bevy_math::Vec3; + + fn test_variable_curve() -> VariableCurve { + let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; + let keyframes = vec![ + Vec3::ONE * 0.0, + Vec3::ONE * 3.0, + Vec3::ONE * 6.0, + Vec3::ONE * 9.0, + ]; + let interpolation = crate::Interpolation::Linear; + + let variable_curve = VariableCurve { + keyframe_timestamps, + keyframes: crate::Keyframes::Translation(keyframes), + interpolation, + }; + + assert!(variable_curve.keyframe_timestamps.len() == variable_curve.keyframes.len()); + + // f32 doesn't impl Ord so we can't easily sort it + let mut maybe_last_timestamp = None; + for current_timestamp in &variable_curve.keyframe_timestamps { + assert!(current_timestamp.is_finite()); + + if let Some(last_timestamp) = maybe_last_timestamp { + assert!(current_timestamp > last_timestamp); + } + maybe_last_timestamp = Some(current_timestamp); + } + + variable_curve + } + + #[test] + fn find_current_keyframe_is_in_bounds() { + let curve = test_variable_curve(); + let min_time = *curve.keyframe_timestamps.first().unwrap(); + // We will always get none at times at or past the second last keyframe + let second_last_keyframe = curve.keyframe_timestamps.len() - 2; + let max_time = curve.keyframe_timestamps[second_last_keyframe]; + let elapsed_time = max_time - min_time; + + let n_keyframes = curve.keyframe_timestamps.len(); + let n_test_points = 5; + + for i in 0..=n_test_points { + // Get a value between 0 and 1 + let normalized_time = i as f32 / n_test_points as f32; + let seek_time = min_time + normalized_time * elapsed_time; + assert!(seek_time >= min_time); + assert!(seek_time <= max_time); + + let maybe_current_keyframe = curve.find_current_keyframe(seek_time); + assert!( + maybe_current_keyframe.is_some(), + "Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}" + ); + + // We cannot return the last keyframe, + // because we want to interpolate between the current and next keyframe + assert!(maybe_current_keyframe.unwrap() < n_keyframes); + } + } + + #[test] + fn find_current_keyframe_returns_none_on_unstarted_animations() { + let curve = test_variable_curve(); + let min_time = *curve.keyframe_timestamps.first().unwrap(); + let seek_time = 0.0; + assert!(seek_time < min_time); + + let maybe_keyframe = curve.find_current_keyframe(seek_time); + assert!( + maybe_keyframe.is_none(), + "Seek time: {seek_time}, Minimum time: {min_time}" + ); + } + + #[test] + fn find_current_keyframe_returns_none_on_finished_animation() { + let curve = test_variable_curve(); + let max_time = *curve.keyframe_timestamps.last().unwrap(); + + assert!(max_time < f32::INFINITY); + let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY); + assert!(maybe_keyframe.is_none()); + + let maybe_keyframe = curve.find_current_keyframe(max_time); + assert!(maybe_keyframe.is_none()); + } + + #[test] + fn second_last_keyframe_is_found_correctly() { + let curve = test_variable_curve(); + + // Exact time match + let second_last_keyframe = curve.keyframe_timestamps.len() - 2; + let second_last_time = curve.keyframe_timestamps[second_last_keyframe]; + let maybe_keyframe = curve.find_current_keyframe(second_last_time); + assert!(maybe_keyframe.unwrap() == second_last_keyframe); + + // Inexact match, between the last and second last frames + let seek_time = second_last_time + 0.001; + let last_time = curve.keyframe_timestamps[second_last_keyframe + 1]; + assert!(seek_time < last_time); + + let maybe_keyframe = curve.find_current_keyframe(seek_time); + assert!(maybe_keyframe.unwrap() == second_last_keyframe); + } + + #[test] + fn exact_keyframe_matches_are_found_correctly() { + let curve = test_variable_curve(); + let second_last_keyframe = curve.keyframes.len() - 2; + + for i in 0..=second_last_keyframe { + let seek_time = curve.keyframe_timestamps[i]; + + let keyframe = curve.find_current_keyframe(seek_time).unwrap(); + assert!(keyframe == i); + } + } + + #[test] + fn exact_and_inexact_keyframes_correspond() { + let curve = test_variable_curve(); + + let second_last_keyframe = curve.keyframes.len() - 2; + + for i in 0..=second_last_keyframe { + let seek_time = curve.keyframe_timestamps[i]; + + let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap(); + + let inexact_seek_time = seek_time + 0.0001; + let final_time = *curve.keyframe_timestamps.last().unwrap(); + assert!(inexact_seek_time < final_time); + + let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap(); + + assert!(exact_keyframe == inexact_keyframe); + } + } +} diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index d2547f731dcc5..f117db2e77e40 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ schedule::{ apply_state_transition, common_conditions::run_once as run_once_condition, run_enter_schedule, InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, - ScheduleBuildSettings, ScheduleLabel, + ScheduleBuildSettings, ScheduleLabel, StateTransitionEvent, }, }; use bevy_utils::{intern::Interned, thiserror::Error, tracing::debug, HashMap, HashSet}; @@ -99,7 +99,7 @@ impl Debug for App { /// /// # Example /// -/// ```rust +/// ``` /// # use bevy_app::{App, AppLabel, SubApp, Main}; /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::schedule::ScheduleLabel; @@ -278,7 +278,7 @@ impl App { /// /// # `run()` might not return /// - /// Calls to [`App::run()`] might never return. + /// Calls to [`App::run()`] will never return on iOS and Web. /// /// In simple and *headless* applications, one can expect that execution will /// proceed, normally, after calling [`run()`](App::run()) but this is not the case for @@ -289,10 +289,7 @@ impl App { /// window is closed and that event loop terminates – behavior of processes that /// do not is often platform dependent or undocumented. /// - /// By default, *Bevy* uses the `winit` crate for window creation. See - /// [`WinitSettings::return_from_run`](https://docs.rs/bevy/latest/bevy/winit/struct.WinitSettings.html#structfield.return_from_run) - /// for further discussion of this topic and for a mechanism to require that [`App::run()`] - /// *does* return – albeit one that carries its own caveats and disclaimers. + /// By default, *Bevy* uses the `winit` crate for window creation. /// /// # Panics /// @@ -351,6 +348,10 @@ impl App { self.plugins_state = PluginsState::Cleaned; } + /// Initializes a [`State`] with standard starting values. + /// + /// If the [`State`] already exists, nothing happens. + /// /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules /// for each state variant (if they don't already exist), an instance of [`apply_state_transition::`] in /// [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and @@ -363,9 +364,47 @@ impl App { /// /// Note that you can also apply state transitions at other points in the schedule /// by adding the [`apply_state_transition`] system manually. - pub fn add_state(&mut self) -> &mut Self { - self.init_resource::>() + pub fn init_state(&mut self) -> &mut Self { + if !self.world.contains_resource::>() { + self.init_resource::>() + .init_resource::>() + .add_event::>() + .add_systems( + StateTransition, + ( + run_enter_schedule::.run_if(run_once_condition()), + apply_state_transition::, + ) + .chain(), + ); + } + + // The OnEnter, OnExit, and OnTransition schedules are lazily initialized + // (i.e. when the first system is added to them), and World::try_run_schedule is used to fail + // gracefully if they aren't present. + + self + } + + /// Inserts a specific [`State`] to the current [`App`] and + /// overrides any [`State`] previously added of the same type. + /// + /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules + /// for each state variant (if they don't already exist), an instance of [`apply_state_transition::`] in + /// [`StateTransition`] so that transitions happen before [`Update`](crate::Update) and + /// a instance of [`run_enter_schedule::`] in [`StateTransition`] with a + /// [`run_once`](`run_once_condition`) condition to run the on enter schedule of the + /// initial state. + /// + /// If you would like to control how other systems run based on the current state, + /// you can emulate this behavior using the [`in_state`] [`Condition`]. + /// + /// Note that you can also apply state transitions at other points in the schedule + /// by adding the [`apply_state_transition`] system manually. + pub fn insert_state(&mut self, state: S) -> &mut Self { + self.insert_resource(State::new(state)) .init_resource::>() + .add_event::>() .add_systems( StateTransition, ( @@ -641,7 +680,7 @@ impl App { /// This vector will be length zero if no plugins of that type have been added. /// If multiple copies of the same plugin are added to the [`App`], they will be listed in insertion order in this vector. /// - /// ```rust + /// ``` /// # use bevy_app::prelude::*; /// # #[derive(Default)] /// # struct ImagePlugin { @@ -718,8 +757,8 @@ impl App { /// Registers the type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource, /// adding reflect data as specified in the [`Reflect`](bevy_reflect::Reflect) derive: - /// ```rust,ignore - /// #[derive(Reflect)] + /// ```ignore (No serde "derive" feature) + /// #[derive(Component, Serialize, Deserialize, Reflect)] /// #[reflect(Component, Serialize, Deserialize)] // will register ReflectComponent, ReflectSerialize, ReflectDeserialize /// ``` /// @@ -739,7 +778,7 @@ impl App { /// this method can be used to insert additional type data. /// /// # Example - /// ```rust + /// ``` /// use bevy_app::App; /// use bevy_reflect::{ReflectSerialize, ReflectDeserialize}; /// @@ -893,7 +932,7 @@ impl App { /// /// ## Example /// - /// ```rust + /// ``` /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings}; @@ -931,7 +970,7 @@ impl App { /// /// ## Example /// - /// ```rust + /// ``` /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::schedule::{LogLevel, ScheduleBuildSettings}; @@ -961,6 +1000,38 @@ impl App { self.world.allow_ambiguous_resource::(); self } + + /// Suppress warnings and errors that would result from systems in these sets having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + /// + /// When possible, do this directly in the `.add_systems(Update, a.ambiguous_with(b))` call. + /// However, sometimes two independant plugins `A` and `B` are reported as ambiguous, which you + /// can only supress as the consumer of both. + #[track_caller] + pub fn ignore_ambiguity( + &mut self, + schedule: impl ScheduleLabel, + a: S1, + b: S2, + ) -> &mut Self + where + S1: IntoSystemSet, + S2: IntoSystemSet, + { + let schedule = schedule.intern(); + let mut schedules = self.world.resource_mut::(); + + if let Some(schedule) = schedules.get_mut(schedule) { + let schedule: &mut Schedule = schedule; + schedule.ignore_ambiguity(a, b); + } else { + let mut new_schedule = Schedule::new(schedule); + new_schedule.ignore_ambiguity(a, b); + schedules.insert(new_schedule); + } + + self + } } fn run_once(mut app: App) { @@ -1071,7 +1142,7 @@ mod tests { #[test] fn add_systems_should_create_schedule_if_it_does_not_exist() { let mut app = App::new(); - app.add_state::() + app.init_state::() .add_systems(OnEnter(AppState::MainMenu), (foo, bar)); app.world.run_schedule(OnEnter(AppState::MainMenu)); @@ -1082,7 +1153,7 @@ mod tests { fn add_systems_should_create_schedule_if_it_does_not_exist2() { let mut app = App::new(); app.add_systems(OnEnter(AppState::MainMenu), (foo, bar)) - .add_state::(); + .init_state::(); app.world.run_schedule(OnEnter(AppState::MainMenu)); assert_eq!(app.world.entities().len(), 2); diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 24872e9950f76..a7b8847cfca2a 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -23,6 +23,17 @@ use bevy_ecs::{ /// * [`Update`] /// * [`PostUpdate`] /// * [`Last`] +/// +/// # Rendering +/// +/// Note rendering is not executed in the main schedule by default. +/// Instead, rendering is performed in a separate [`SubApp`](crate::app::SubApp) +/// which exchanges data with the main app in between the main schedule runs. +/// +/// See [`RenderPlugin`] and [`PipelinedRenderingPlugin`] for more details. +/// +/// [`RenderPlugin`]: https://docs.rs/bevy/latest/bevy/render/struct.RenderPlugin.html +/// [`PipelinedRenderingPlugin`]: https://docs.rs/bevy/latest/bevy/render/pipelined_rendering/struct.PipelinedRenderingPlugin.html #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct Main; diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index b722737ccb56a..55e1ce29a24a8 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -25,8 +25,8 @@ pub trait Plugin: Downcast + Any + Send + Sync { /// Configures the [`App`] to which this plugin is added. fn build(&self, app: &mut App); - /// Has the plugin finished it's setup? This can be useful for plugins that needs something - /// asynchronous to happen before they can finish their setup, like renderer initialization. + /// Has the plugin finished its setup? This can be useful for plugins that need something + /// asynchronous to happen before they can finish their setup, like the initialization of a renderer. /// Once the plugin is ready, [`finish`](Plugin::finish) should be called. fn ready(&self, _app: &App) -> bool { true diff --git a/crates/bevy_app/src/plugin_group.rs b/crates/bevy_app/src/plugin_group.rs index a299c0a4f1583..257f869d12b3d 100644 --- a/crates/bevy_app/src/plugin_group.rs +++ b/crates/bevy_app/src/plugin_group.rs @@ -194,7 +194,7 @@ impl PluginGroupBuilder { } /// A plugin group which doesn't do anything. Useful for examples: -/// ```rust +/// ``` /// # use bevy_app::prelude::*; /// use bevy_app::NoopPluginGroup as MinimalPlugins; /// diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index 3017ea3f12dab..cc1ee6bf097a5 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -1,5 +1,7 @@ -use crate::{self as bevy_asset, LoadState}; -use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle}; +use crate::{self as bevy_asset}; +use crate::{ + Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, LoadState, UntypedHandle, +}; use bevy_ecs::{ prelude::EventWriter, system::{Res, ResMut, Resource}, @@ -297,6 +299,11 @@ impl Assets { self.handle_provider.clone() } + /// Reserves a new [`Handle`] for an asset that will be stored in this collection. + pub fn reserve_handle(&self) -> Handle { + self.handle_provider.reserve_handle().typed::() + } + /// Inserts the given `asset`, identified by the given `id`. If an asset already exists for `id`, it will be replaced. pub fn insert(&mut self, id: impl Into>, asset: A) { let id: AssetId = id.into(); @@ -359,9 +366,9 @@ impl Assets { /// Adds the given `asset` and allocates a new strong [`Handle`] for it. #[inline] - pub fn add(&mut self, asset: A) -> Handle { + pub fn add(&mut self, asset: impl Into) -> Handle { let index = self.dense_storage.allocator.reserve(); - self.insert_with_index(index, asset).unwrap(); + self.insert_with_index(index, asset.into()).unwrap(); Handle::Strong( self.handle_provider .get_handle(index.into(), false, None, None), @@ -484,9 +491,7 @@ impl Assets { } /// A system that synchronizes the state of assets in this collection with the [`AssetServer`]. This manages - /// [`Handle`] drop events and adds queued [`AssetEvent`] values to their [`Events`] resource. - /// - /// [`Events`]: bevy_ecs::event::Events + /// [`Handle`] drop events. pub fn track_assets(mut assets: ResMut, asset_server: Res) { let assets = &mut *assets; // note that we must hold this lock for the entire duration of this function to ensure @@ -496,10 +501,13 @@ impl Assets { let mut infos = asset_server.data.infos.write(); let mut not_ready = Vec::new(); while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() { - let id = drop_event.id; + let id = drop_event.id.typed(); + + assets.queued_events.push(AssetEvent::Unused { id }); + if drop_event.asset_server_managed { - let untyped = id.untyped(TypeId::of::()); - if let Some(info) = infos.get(untyped) { + let untyped_id = drop_event.id.untyped(TypeId::of::()); + if let Some(info) = infos.get(untyped_id) { if info.load_state == LoadState::Loading || info.load_state == LoadState::NotLoaded { @@ -507,13 +515,14 @@ impl Assets { continue; } } - if infos.process_handle_drop(untyped) { - assets.remove_dropped(id.typed()); + if infos.process_handle_drop(untyped_id) { + assets.remove_dropped(id); } } else { - assets.remove_dropped(id.typed()); + assets.remove_dropped(id); } } + // TODO: this is _extremely_ inefficient find a better fix // This will also loop failed assets indefinitely. Is that ok? for event in not_ready { diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index 96f10a9c6f5ab..f049751bb79c3 100644 --- a/crates/bevy_asset/src/event.rs +++ b/crates/bevy_asset/src/event.rs @@ -1,8 +1,47 @@ -use crate::{Asset, AssetId}; +use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; use bevy_ecs::event::Event; use std::fmt::Debug; -/// Events that occur for a specific [`Asset`], such as "value changed" events and "dependency" events. +/// An event emitted when a specific [`Asset`] fails to load. +/// +/// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. +#[derive(Event, Clone, Debug)] +pub struct AssetLoadFailedEvent { + pub id: AssetId, + /// The asset path that was attempted. + pub path: AssetPath<'static>, + /// Why the asset failed to load. + pub error: AssetLoadError, +} + +impl AssetLoadFailedEvent { + /// Converts this to an "untyped" / "generic-less" asset error event that stores the type information. + pub fn untyped(&self) -> UntypedAssetLoadFailedEvent { + self.into() + } +} + +/// An untyped version of [`AssetLoadFailedEvent`]. +#[derive(Event, Clone, Debug)] +pub struct UntypedAssetLoadFailedEvent { + pub id: UntypedAssetId, + /// The asset path that was attempted. + pub path: AssetPath<'static>, + /// Why the asset failed to load. + pub error: AssetLoadError, +} + +impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent { + fn from(value: &AssetLoadFailedEvent) -> Self { + UntypedAssetLoadFailedEvent { + id: value.id.untyped(), + path: value.path.clone(), + error: value.error.clone(), + } + } +} + +/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. #[derive(Event)] pub enum AssetEvent { /// Emitted whenever an [`Asset`] is added. @@ -11,6 +50,8 @@ pub enum AssetEvent { Modified { id: AssetId }, /// Emitted whenever an [`Asset`] is removed. Removed { id: AssetId }, + /// Emitted when the last [`super::Handle::Strong`] of an [`Asset`] is dropped. + Unused { id: AssetId }, /// Emitted whenever an [`Asset`] has been fully loaded (including its dependencies and all "recursive dependencies"). LoadedWithDependencies { id: AssetId }, } @@ -35,6 +76,11 @@ impl AssetEvent { pub fn is_removed(&self, asset_id: impl Into>) -> bool { matches!(self, AssetEvent::Removed { id } if *id == asset_id.into()) } + + /// Returns `true` if this event is [`AssetEvent::Unused`] and matches the given `id`. + pub fn is_unused(&self, asset_id: impl Into>) -> bool { + matches!(self, AssetEvent::Unused { id } if *id == asset_id.into()) + } } impl Clone for AssetEvent { @@ -51,6 +97,7 @@ impl Debug for AssetEvent { Self::Added { id } => f.debug_struct("Added").field("id", id).finish(), Self::Modified { id } => f.debug_struct("Modified").field("id", id).finish(), Self::Removed { id } => f.debug_struct("Removed").field("id", id).finish(), + Self::Unused { id } => f.debug_struct("Unused").field("id", id).finish(), Self::LoadedWithDependencies { id } => f .debug_struct("LoadedWithDependencies") .field("id", id) @@ -65,6 +112,7 @@ impl PartialEq for AssetEvent { (Self::Added { id: l_id }, Self::Added { id: r_id }) | (Self::Modified { id: l_id }, Self::Modified { id: r_id }) | (Self::Removed { id: l_id }, Self::Removed { id: r_id }) + | (Self::Unused { id: l_id }, Self::Unused { id: r_id }) | ( Self::LoadedWithDependencies { id: l_id }, Self::LoadedWithDependencies { id: r_id }, diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 103475f8baa60..1f9b8fb6084df 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -124,7 +124,7 @@ impl std::fmt::Debug for StrongHandle { #[reflect(Component)] pub enum Handle { /// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept - /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. + /// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata. Strong(Arc), /// A "weak" reference to an [`Asset`]. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`], /// nor will it keep assets alive. @@ -189,7 +189,7 @@ impl Handle { /// Converts this [`Handle`] to an "untyped" / "generic-less" [`UntypedHandle`], which stores the [`Asset`] type information /// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Weak`] for - /// [`Handle::Weak`]. + /// [`Handle::Weak`]. #[inline] pub fn untyped(self) -> UntypedHandle { self.into() diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index 3c5902e95592e..f792a72b00383 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -29,7 +29,7 @@ impl AssetReader for AndroidAssetReader { let mut opened_asset = asset_manager .open(&CString::new(path.to_str().unwrap()).unwrap()) .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; - let bytes = opened_asset.get_buffer()?; + let bytes = opened_asset.buffer()?; let reader: Box = Box::new(VecReader::new(bytes.to_vec())); Ok(reader) }) @@ -48,7 +48,7 @@ impl AssetReader for AndroidAssetReader { let mut opened_asset = asset_manager .open(&CString::new(meta_path.to_str().unwrap()).unwrap()) .ok_or(AssetReaderError::NotFound(meta_path))?; - let bytes = opened_asset.get_buffer()?; + let bytes = opened_asset.buffer()?; let reader: Box = Box::new(VecReader::new(bytes.to_vec())); Ok(reader) }) diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index 0448a7cdd83a7..80340ac9debbc 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -199,14 +199,28 @@ macro_rules! embedded_asset { .world .resource_mut::<$crate::io::embedded::EmbeddedAssetRegistry>(); let path = $crate::embedded_path!($source_path, $path); - #[cfg(feature = "embedded_watcher")] - let full_path = std::path::Path::new(file!()).parent().unwrap().join($path); - #[cfg(not(feature = "embedded_watcher"))] - let full_path = std::path::PathBuf::new(); - embedded.insert_asset(full_path, &path, include_bytes!($path)); + let watched_path = $crate::io::embedded::watched_path(file!(), $path); + embedded.insert_asset(watched_path, &path, include_bytes!($path)); }}; } +/// Returns the path used by the watcher. +#[doc(hidden)] +#[cfg(feature = "embedded_watcher")] +pub fn watched_path(source_file_path: &'static str, asset_path: &'static str) -> PathBuf { + PathBuf::from(source_file_path) + .parent() + .unwrap() + .join(asset_path) +} + +/// Returns an empty PathBuf. +#[doc(hidden)] +#[cfg(not(feature = "embedded_watcher"))] +pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) -> PathBuf { + PathBuf::from("") +} + /// Loads an "internal" asset by embedding the string stored in the given `path_str` and associates it with the given handle. #[macro_export] macro_rules! load_internal_asset { diff --git a/crates/bevy_asset/src/io/file/file_asset.rs b/crates/bevy_asset/src/io/file/file_asset.rs index 7fd95cde358b4..aa20913140111 100644 --- a/crates/bevy_asset/src/io/file/file_asset.rs +++ b/crates/bevy_asset/src/io/file/file_asset.rs @@ -155,70 +155,70 @@ impl AssetWriter for FileAssetWriter { }) } - fn remove_directory<'a>( + fn rename<'a>( &'a self, - path: &'a Path, + old_path: &'a Path, + new_path: &'a Path, ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir_all(full_path).await?; + let full_old_path = self.root_path.join(old_path); + let full_new_path = self.root_path.join(new_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; Ok(()) }) } - fn remove_empty_directory<'a>( + fn rename_meta<'a>( &'a self, - path: &'a Path, + old_path: &'a Path, + new_path: &'a Path, ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { Box::pin(async move { - let full_path = self.root_path.join(path); - async_fs::remove_dir(full_path).await?; + let old_meta_path = get_meta_path(old_path); + let new_meta_path = get_meta_path(new_path); + let full_old_path = self.root_path.join(old_meta_path); + let full_new_path = self.root_path.join(new_meta_path); + if let Some(parent) = full_new_path.parent() { + async_fs::create_dir_all(parent).await?; + } + async_fs::rename(full_old_path, full_new_path).await?; Ok(()) }) } - fn remove_assets_in_directory<'a>( + fn remove_directory<'a>( &'a self, path: &'a Path, ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { Box::pin(async move { let full_path = self.root_path.join(path); - async_fs::remove_dir_all(&full_path).await?; - async_fs::create_dir_all(&full_path).await?; + async_fs::remove_dir_all(full_path).await?; Ok(()) }) } - fn rename<'a>( + fn remove_empty_directory<'a>( &'a self, - old_path: &'a Path, - new_path: &'a Path, + path: &'a Path, ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { Box::pin(async move { - let full_old_path = self.root_path.join(old_path); - let full_new_path = self.root_path.join(new_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; + let full_path = self.root_path.join(path); + async_fs::remove_dir(full_path).await?; Ok(()) }) } - fn rename_meta<'a>( + fn remove_assets_in_directory<'a>( &'a self, - old_path: &'a Path, - new_path: &'a Path, + path: &'a Path, ) -> BoxedFuture<'a, Result<(), AssetWriterError>> { Box::pin(async move { - let old_meta_path = get_meta_path(old_path); - let new_meta_path = get_meta_path(new_path); - let full_old_path = self.root_path.join(old_meta_path); - let full_new_path = self.root_path.join(new_meta_path); - if let Some(parent) = full_new_path.parent() { - async_fs::create_dir_all(parent).await?; - } - async_fs::rename(full_old_path, full_new_path).await?; + let full_path = self.root_path.join(path); + async_fs::remove_dir_all(&full_path).await?; + async_fs::create_dir_all(&full_path).await?; Ok(()) }) } diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 63a28cbb96a69..6ee59fab37d24 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -6,7 +6,7 @@ mod file_asset; #[cfg(not(feature = "multi-threaded"))] mod sync_file_asset; -use bevy_log::warn; +use bevy_log::error; #[cfg(feature = "file_watcher")] pub use file_watcher::*; @@ -45,12 +45,6 @@ impl FileAssetReader { /// See `get_base_path` below. pub fn new>(path: P) -> Self { let root_path = Self::get_base_path().join(path.as_ref()); - if let Err(e) = std::fs::create_dir_all(&root_path) { - warn!( - "Failed to create root directory {:?} for file asset reader: {:?}", - root_path, e - ); - } Self { root_path } } @@ -80,7 +74,15 @@ impl FileAssetWriter { /// watching for changes. /// /// See `get_base_path` below. - pub fn new>(path: P) -> Self { + pub fn new + std::fmt::Debug>(path: P, create_root: bool) -> Self { + if create_root { + if let Err(e) = std::fs::create_dir_all(&path) { + error!( + "Failed to create root directory {:?} for file asset writer: {:?}", + path, e + ); + } + } Self { root_path: get_base_path().join(path.as_ref()), } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 742dd40b8b071..c03afdabf100d 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -27,20 +27,32 @@ use futures_lite::{ready, Stream}; use std::{ path::{Path, PathBuf}, pin::Pin, + sync::Arc, task::Poll, }; use thiserror::Error; /// Errors that occur while loading assets. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum AssetReaderError { /// Path not found. - #[error("path not found: {0}")] + #[error("Path not found: {0}")] NotFound(PathBuf), /// Encountered an I/O error while loading an asset. - #[error("encountered an io error while loading asset: {0}")] - Io(#[from] std::io::Error), + #[error("Encountered an I/O error while loading asset: {0}")] + Io(Arc), + + /// The HTTP request completed but returned an unhandled [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). + /// If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`]. + #[error("Encountered HTTP status {0:?} when loading asset")] + HttpError(u16), +} + +impl From for AssetReaderError { + fn from(value: std::io::Error) -> Self { + Self::Io(Arc::new(value)) + } } pub type Reader<'a> = dyn AsyncRead + Unpin + Send + Sync + 'a; diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index b409579344a32..57f6170d17b3e 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -43,7 +43,7 @@ impl<'a> AssetSourceId<'a> { } /// Returns [`None`] if this is [`AssetSourceId::Default`] and [`Some`] containing the - /// the name if this is [`AssetSourceId::Name`]. + /// name if this is [`AssetSourceId::Name`]. pub fn as_str(&self) -> Option<&str> { match self { AssetSourceId::Default => None, @@ -111,7 +111,7 @@ impl<'a> PartialEq for AssetSourceId<'a> { #[derive(Default)] pub struct AssetSourceBuilder { pub reader: Option Box + Send + Sync>>, - pub writer: Option Option> + Send + Sync>>, + pub writer: Option Option> + Send + Sync>>, pub watcher: Option< Box< dyn FnMut(crossbeam_channel::Sender) -> Option> @@ -120,7 +120,8 @@ pub struct AssetSourceBuilder { >, >, pub processed_reader: Option Box + Send + Sync>>, - pub processed_writer: Option Option> + Send + Sync>>, + pub processed_writer: + Option Option> + Send + Sync>>, pub processed_watcher: Option< Box< dyn FnMut(crossbeam_channel::Sender) -> Option> @@ -141,14 +142,14 @@ impl AssetSourceBuilder { watch: bool, watch_processed: bool, ) -> Option { - let reader = (self.reader.as_mut()?)(); - let writer = self.writer.as_mut().and_then(|w| (w)()); - let processed_writer = self.processed_writer.as_mut().and_then(|w| (w)()); + let reader = self.reader.as_mut()?(); + let writer = self.writer.as_mut().and_then(|w| w(false)); + let processed_writer = self.processed_writer.as_mut().and_then(|w| w(true)); let mut source = AssetSource { id: id.clone(), reader, writer, - processed_reader: self.processed_reader.as_mut().map(|r| (r)()), + processed_reader: self.processed_reader.as_mut().map(|r| r()), processed_writer, event_receiver: None, watcher: None, @@ -158,7 +159,7 @@ impl AssetSourceBuilder { if watch { let (sender, receiver) = crossbeam_channel::unbounded(); - match self.watcher.as_mut().and_then(|w| (w)(sender)) { + match self.watcher.as_mut().and_then(|w| w(sender)) { Some(w) => { source.watcher = Some(w); source.event_receiver = Some(receiver); @@ -173,7 +174,7 @@ impl AssetSourceBuilder { if watch_processed { let (sender, receiver) = crossbeam_channel::unbounded(); - match self.processed_watcher.as_mut().and_then(|w| (w)(sender)) { + match self.processed_watcher.as_mut().and_then(|w| w(sender)) { Some(w) => { source.processed_watcher = Some(w); source.processed_event_receiver = Some(receiver); @@ -200,7 +201,7 @@ impl AssetSourceBuilder { /// Will use the given `writer` function to construct unprocessed [`AssetWriter`] instances. pub fn with_writer( mut self, - writer: impl FnMut() -> Option> + Send + Sync + 'static, + writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, ) -> Self { self.writer = Some(Box::new(writer)); self @@ -230,7 +231,7 @@ impl AssetSourceBuilder { /// Will use the given `writer` function to construct processed [`AssetWriter`] instances. pub fn with_processed_writer( mut self, - writer: impl FnMut() -> Option> + Send + Sync + 'static, + writer: impl FnMut(bool) -> Option> + Send + Sync + 'static, ) -> Self { self.processed_writer = Some(Box::new(writer)); self @@ -443,10 +444,13 @@ impl AssetSource { /// the asset root. This will return [`None`] if this platform does not support writing assets by default. pub fn get_default_writer( _path: String, - ) -> impl FnMut() -> Option> + Send + Sync { - move || { + ) -> impl FnMut(bool) -> Option> + Send + Sync { + move |_create_root: bool| { #[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))] - return Some(Box::new(super::file::FileAssetWriter::new(&_path))); + return Some(Box::new(super::file::FileAssetWriter::new( + &_path, + _create_root, + ))); #[cfg(any(target_arch = "wasm32", target_os = "android"))] return None; } @@ -486,7 +490,7 @@ impl AssetSource { sender, file_debounce_wait_time, ) - .unwrap(), + .expect("Failed to create file watcher"), )); #[cfg(any( not(feature = "file_watcher"), @@ -569,22 +573,22 @@ impl AssetSources { } /// An error returned when an [`AssetSource`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not exist")] pub struct MissingAssetSourceError(AssetSourceId<'static>); /// An error returned when an [`AssetWriter`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not have an AssetWriter.")] pub struct MissingAssetWriterError(AssetSourceId<'static>); /// An error returned when a processed [`AssetReader`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not have a processed AssetReader.")] pub struct MissingProcessedAssetReaderError(AssetSourceId<'static>); /// An error returned when a processed [`AssetWriter`] does not exist for a given id. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("Asset Source '{0}' does not have a processed AssetWriter.")] pub struct MissingProcessedAssetWriterError(AssetSourceId<'static>); diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 2636419299f81..7ee60ecfdb8c9 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -53,10 +53,7 @@ impl HttpWasmAssetReader { Ok(reader) } 404 => Err(AssetReaderError::NotFound(path)), - status => Err(AssetReaderError::Io(std::io::Error::new( - std::io::ErrorKind::Other, - format!("Encountered unexpected HTTP status {status}"), - ))), + status => Err(AssetReaderError::HttpError(status as u16)), } } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 552e69a134df0..aadccccf54fed 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -100,7 +100,7 @@ pub enum AssetMode { /// /// When developing an app, you should enable the `asset_processor` cargo feature, which will run the asset processor at startup. This should generally /// be used in combination with the `file_watcher` cargo feature, which enables hot-reloading of assets that have changed. When both features are enabled, - /// changes to "original/source assets" will be detected, the asset will be re-processed, and then the final processed asset will be hot-reloaded in the app. + /// changes to "original/source assets" will be detected, the asset will be re-processed, and then the final processed asset will be hot-reloaded in the app. /// /// [`AssetMeta`]: meta::AssetMeta /// [`AssetSource`]: io::AssetSource @@ -213,6 +213,7 @@ impl Plugin for AssetPlugin { .init_asset::() .init_asset::() .init_asset::<()>() + .add_event::() .configure_sets( UpdateAssets, TrackAssets.after(handle_internal_asset_events), @@ -320,6 +321,40 @@ impl AssetApp for App { self } + fn register_asset_processor(&mut self, processor: P) -> &mut Self { + if let Some(asset_processor) = self.world.get_resource::() { + asset_processor.register_processor(processor); + } + self + } + + fn register_asset_source( + &mut self, + id: impl Into>, + source: AssetSourceBuilder, + ) -> &mut Self { + let id = id.into(); + if self.world.get_resource::().is_some() { + error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id); + } + + { + let mut sources = self + .world + .get_resource_or_insert_with(AssetSourceBuilders::default); + sources.insert(id, source); + } + + self + } + + fn set_default_asset_processor(&mut self, extension: &str) -> &mut Self { + if let Some(asset_processor) = self.world.get_resource::() { + asset_processor.set_default_processor::

(extension); + } + self + } + fn init_asset_loader(&mut self) -> &mut Self { let loader = L::from_world(&mut self.world); self.register_asset_loader(loader) @@ -343,6 +378,7 @@ impl AssetApp for App { self.insert_resource(assets) .allow_ambiguous_resource::>() .add_event::>() + .add_event::>() .register_type::>() .register_type::>() .add_systems(AssetEvents, Assets::::asset_events) @@ -372,40 +408,6 @@ impl AssetApp for App { .preregister_loader::(extensions); self } - - fn register_asset_processor(&mut self, processor: P) -> &mut Self { - if let Some(asset_processor) = self.world.get_resource::() { - asset_processor.register_processor(processor); - } - self - } - - fn set_default_asset_processor(&mut self, extension: &str) -> &mut Self { - if let Some(asset_processor) = self.world.get_resource::() { - asset_processor.set_default_processor::

(extension); - } - self - } - - fn register_asset_source( - &mut self, - id: impl Into>, - source: AssetSourceBuilder, - ) -> &mut Self { - let id = id.into(); - if self.world.get_resource::().is_some() { - error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id); - } - - { - let mut sources = self - .world - .get_resource_or_insert_with(AssetSourceBuilders::default); - sources.insert(id, source); - } - - self - } } /// A system set that holds all "track asset" operations. @@ -431,11 +433,12 @@ mod tests { io::{ gated::{GateOpener, GatedReader}, memory::{Dir, MemoryAssetReader}, - AssetSource, AssetSourceId, Reader, + AssetReader, AssetReaderError, AssetSource, AssetSourceId, Reader, }, loader::{AssetLoader, LoadContext}, - Asset, AssetApp, AssetEvent, AssetId, AssetPath, AssetPlugin, AssetServer, Assets, - DependencyLoadState, LoadState, RecursiveDependencyLoadState, + Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, + AssetPlugin, AssetServer, Assets, DependencyLoadState, LoadState, + RecursiveDependencyLoadState, }; use bevy_app::{App, Update}; use bevy_core::TaskPoolPlugin; @@ -446,20 +449,23 @@ mod tests { }; use bevy_log::LogPlugin; use bevy_reflect::TypePath; - use bevy_utils::BoxedFuture; + use bevy_utils::{BoxedFuture, Duration, HashMap}; use futures_lite::AsyncReadExt; use serde::{Deserialize, Serialize}; - use std::path::Path; + use std::{ + path::{Path, PathBuf}, + sync::Arc, + }; use thiserror::Error; #[derive(Asset, TypePath, Debug)] pub struct CoolText { - text: String, - embedded: String, + pub text: String, + pub embedded: String, #[dependency] - dependencies: Vec>, + pub dependencies: Vec>, #[dependency] - sub_texts: Vec>, + pub sub_texts: Vec>, } #[derive(Asset, TypePath, Debug)] @@ -476,10 +482,10 @@ mod tests { } #[derive(Default)] - struct CoolTextLoader; + pub struct CoolTextLoader; #[derive(Error, Debug)] - enum CoolTextLoaderError { + pub enum CoolTextLoaderError { #[error("Could not load dependency: {dependency}")] CannotLoadDependency { dependency: AssetPath<'static> }, #[error("A RON error occurred during loading")] @@ -537,6 +543,83 @@ mod tests { } } + /// A dummy [`CoolText`] asset reader that only succeeds after `failure_count` times it's read from for each asset. + #[derive(Default, Clone)] + pub struct UnstableMemoryAssetReader { + pub attempt_counters: Arc>>, + pub load_delay: Duration, + memory_reader: MemoryAssetReader, + failure_count: usize, + } + + impl UnstableMemoryAssetReader { + pub fn new(root: Dir, failure_count: usize) -> Self { + Self { + load_delay: Duration::from_millis(10), + memory_reader: MemoryAssetReader { root }, + attempt_counters: Default::default(), + failure_count, + } + } + } + + impl AssetReader for UnstableMemoryAssetReader { + fn is_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result> { + self.memory_reader.is_directory(path) + } + fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result, AssetReaderError>> { + self.memory_reader.read_directory(path) + } + fn read_meta<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture<'a, Result>, AssetReaderError>> { + self.memory_reader.read_meta(path) + } + fn read<'a>( + &'a self, + path: &'a Path, + ) -> BoxedFuture< + 'a, + Result>, bevy_asset::io::AssetReaderError>, + > { + let attempt_number = { + let key = PathBuf::from(path); + let mut attempt_counters = self.attempt_counters.lock().unwrap(); + if let Some(existing) = attempt_counters.get_mut(&key) { + *existing += 1; + *existing + } else { + attempt_counters.insert(key, 1); + 1 + } + }; + + if attempt_number <= self.failure_count { + let io_error = std::io::Error::new( + std::io::ErrorKind::ConnectionRefused, + format!( + "Simulated failure {attempt_number} of {}", + self.failure_count + ), + ); + let wait = self.load_delay; + return Box::pin(async move { + std::thread::sleep(wait); + Err(AssetReaderError::Io(io_error.into())) + }); + } + + self.memory_reader.read(path) + } + } + fn test_app(dir: Dir) -> (App, GateOpener) { let mut app = App::new(); let (gated_memory_reader, gate_opener) = GatedReader::new(MemoryAssetReader { root: dir }); @@ -552,7 +635,7 @@ mod tests { (app, gate_opener) } - fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) { + pub fn run_app_until(app: &mut App, mut predicate: impl FnMut(&mut World) -> Option<()>) { for _ in 0..LARGE_ITERATION_COUNT { app.update(); if predicate(&mut app.world).is_some() { @@ -581,6 +664,10 @@ mod tests { #[test] fn load_dependencies() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[cfg(not(feature = "multi-threaded"))] + panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + let dir = Dir::default(); let a_path = "a.cool.ron"; @@ -872,13 +959,23 @@ mod tests { id: id_results.d_id, }, AssetEvent::Modified { id: a_id }, + AssetEvent::Unused { id: a_id }, AssetEvent::Removed { id: a_id }, + AssetEvent::Unused { + id: id_results.b_id, + }, AssetEvent::Removed { id: id_results.b_id, }, + AssetEvent::Unused { + id: id_results.c_id, + }, AssetEvent::Removed { id: id_results.c_id, }, + AssetEvent::Unused { + id: id_results.d_id, + }, AssetEvent::Removed { id: id_results.d_id, }, @@ -888,6 +985,10 @@ mod tests { #[test] fn failure_load_states() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[cfg(not(feature = "multi-threaded"))] + panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + let dir = Dir::default(); let a_path = "a.cool.ron"; @@ -1007,6 +1108,10 @@ mod tests { #[test] fn manual_asset_management() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[cfg(not(feature = "multi-threaded"))] + panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + let dir = Dir::default(); let dep_path = "dep.cool.ron"; @@ -1062,7 +1167,11 @@ mod tests { // remove event is emitted app.update(); let events = std::mem::take(&mut app.world.resource_mut::().0); - let expected_events = vec![AssetEvent::Added { id }, AssetEvent::Removed { id }]; + let expected_events = vec![ + AssetEvent::Added { id }, + AssetEvent::Unused { id }, + AssetEvent::Removed { id }, + ]; assert_eq!(events, expected_events); let dep_handle = app.world.resource::().load(dep_path); @@ -1108,6 +1217,10 @@ mod tests { #[test] fn load_folder() { + // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded + #[cfg(not(feature = "multi-threaded"))] + panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + let dir = Dir::default(); let a_path = "text/a.cool.ron"; @@ -1196,6 +1309,133 @@ mod tests { }); } + /// Tests that `AssetLoadFailedEvent` events are emitted and can be used to retry failed assets. + #[test] + fn load_error_events() { + #[derive(Resource, Default)] + struct ErrorTracker { + tick: u64, + failures: usize, + queued_retries: Vec<(AssetPath<'static>, AssetId, u64)>, + finished_asset: Option>, + } + + fn asset_event_handler( + mut events: EventReader>, + mut tracker: ResMut, + ) { + for event in events.read() { + if let AssetEvent::LoadedWithDependencies { id } = event { + tracker.finished_asset = Some(*id); + } + } + } + + fn asset_load_error_event_handler( + server: Res, + mut errors: EventReader>, + mut tracker: ResMut, + ) { + // In the real world, this would refer to time (not ticks) + tracker.tick += 1; + + // Retry loading past failed items + let now = tracker.tick; + tracker + .queued_retries + .retain(|(path, old_id, retry_after)| { + if now > *retry_after { + let new_handle = server.load::(path); + assert_eq!(&new_handle.id(), old_id); + false + } else { + true + } + }); + + // Check what just failed + for error in errors.read() { + let (load_state, _, _) = server.get_load_states(error.id).unwrap(); + assert_eq!(load_state, LoadState::Failed); + assert_eq!(*error.path.source(), AssetSourceId::Name("unstable".into())); + match &error.error { + AssetLoadError::AssetReaderError(read_error) => match read_error { + AssetReaderError::Io(_) => { + tracker.failures += 1; + if tracker.failures <= 2 { + // Retry in 10 ticks + tracker.queued_retries.push(( + error.path.clone(), + error.id, + now + 10, + )); + } else { + panic!( + "Unexpected failure #{} (expected only 2)", + tracker.failures + ); + } + } + _ => panic!("Unexpected error type {:?}", read_error), + }, + _ => panic!("Unexpected error type {:?}", error.error), + } + } + } + + let a_path = "text/a.cool.ron"; + let a_ron = r#" +( + text: "a", + dependencies: [], + embedded_dependencies: [], + sub_texts: [], +)"#; + + let dir = Dir::default(); + dir.insert_asset_text(Path::new(a_path), a_ron); + let unstable_reader = UnstableMemoryAssetReader::new(dir, 2); + + let mut app = App::new(); + app.register_asset_source( + "unstable", + AssetSource::build().with_reader(move || Box::new(unstable_reader.clone())), + ) + .add_plugins(( + TaskPoolPlugin::default(), + LogPlugin::default(), + AssetPlugin::default(), + )) + .init_asset::() + .register_asset_loader(CoolTextLoader) + .init_resource::() + .add_systems( + Update, + (asset_event_handler, asset_load_error_event_handler).chain(), + ); + + let asset_server = app.world.resource::().clone(); + let a_path = format!("unstable://{a_path}"); + let a_handle: Handle = asset_server.load(a_path); + let a_id = a_handle.id(); + + app.world.spawn(a_handle); + + run_app_until(&mut app, |world| { + let tracker = world.resource::(); + match tracker.finished_asset { + Some(asset_id) => { + assert_eq!(asset_id, a_id); + let assets = world.resource::>(); + let result = assets.get(asset_id).unwrap(); + assert_eq!(result.text, "a"); + Some(()) + } + None => None, + } + }); + } + #[test] fn ignore_system_ambiguities_on_assets() { let mut app = App::new(); diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 26f7ebc6a0f8f..da3468201e7a0 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -97,6 +97,10 @@ where }) } + fn extensions(&self) -> &[&str] { + ::extensions(self) + } + fn deserialize_meta(&self, meta: &[u8]) -> Result, DeserializeMetaError> { let meta = AssetMeta::::deserialize(meta)?; Ok(Box::new(meta)) @@ -109,10 +113,6 @@ where })) } - fn extensions(&self) -> &[&str] { - ::extensions(self) - } - fn type_name(&self) -> &'static str { std::any::type_name::() } @@ -121,13 +121,13 @@ where TypeId::of::() } - fn asset_type_id(&self) -> TypeId { - TypeId::of::() - } - fn asset_type_name(&self) -> &'static str { std::any::type_name::() } + + fn asset_type_id(&self) -> TypeId { + TypeId::of::() + } } pub(crate) struct LabeledAsset { @@ -254,7 +254,7 @@ pub struct LoadDirectError { } /// An error that occurs while deserializing [`AssetMeta`]. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum DeserializeMetaError { #[error("Failed to deserialize asset meta: {0:?}")] DeserializeSettings(#[from] SpannedError), diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index dbcd7d7feb57d..e8758999f7d7e 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -128,11 +128,6 @@ pub trait AssetMetaDyn: Downcast + Send + Sync { } impl AssetMetaDyn for AssetMeta { - fn serialize(&self) -> Vec { - ron::ser::to_string_pretty(&self, PrettyConfig::default()) - .expect("type is convertible to ron") - .into_bytes() - } fn loader_settings(&self) -> Option<&dyn Settings> { if let AssetAction::Load { settings, .. } = &self.asset { Some(settings) @@ -147,6 +142,11 @@ impl AssetMetaDyn for AssetMeta { None } } + fn serialize(&self) -> Vec { + ron::ser::to_string_pretty(&self, PrettyConfig::default()) + .expect("type is convertible to ron") + .into_bytes() + } fn processed_info(&self) -> &Option { &self.processed_info } diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index f321fc2154a8d..68991316913db 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -302,7 +302,7 @@ impl<'a> AssetPath<'a> { /// Resolves a relative asset path via concatenation. The result will be an `AssetPath` which /// is resolved relative to this "base" path. /// - /// ```rust + /// ``` /// # use bevy_asset::AssetPath; /// assert_eq!(AssetPath::parse("a/b").resolve("c"), Ok(AssetPath::parse("a/b/c"))); /// assert_eq!(AssetPath::parse("a/b").resolve("./c"), Ok(AssetPath::parse("a/b/c"))); @@ -352,7 +352,7 @@ impl<'a> AssetPath<'a> { /// primary use case for this method is resolving relative paths embedded within asset files, /// which are relative to the asset in which they are contained. /// - /// ```rust + /// ``` /// # use bevy_asset::AssetPath; /// assert_eq!(AssetPath::parse("a/b").resolve_embed("c"), Ok(AssetPath::parse("a/c"))); /// assert_eq!(AssetPath::parse("a/b").resolve_embed("./c"), Ok(AssetPath::parse("a/c"))); @@ -626,10 +626,6 @@ impl Reflect for AssetPath<'static> { self } #[inline] - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - #[inline] fn apply(&mut self, value: &dyn Reflect) { let value = Reflect::as_any(value); if let Some(value) = value.downcast_ref::() { @@ -655,6 +651,10 @@ impl Reflect for AssetPath<'static> { fn reflect_owned(self: Box) -> ReflectOwned { ReflectOwned::Value(self) } + #[inline] + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } fn reflect_hash(&self) -> Option { let mut hasher = bevy_reflect::utility::reflect_hasher(); Hash::hash(&::core::any::Any::type_id(self), &mut hasher); diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 9033a3b08abfa..b5ef275103ac3 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -293,6 +293,13 @@ impl AssetProcessor { AssetPath::from_path(&path).with_source(source.id()) ); } + AssetReaderError::HttpError(status) => { + error!( + "Path '{}' was removed, but the destination reader could not determine if it \ + was a folder or a file due to receiving an unexpected HTTP Status {status}", + AssetPath::from_path(&path).with_source(source.id()) + ); + } } } } @@ -344,6 +351,13 @@ impl AssetProcessor { AssetReaderError::NotFound(_err) => { // The processed folder does not exist. No need to update anything } + AssetReaderError::HttpError(status) => { + self.log_unrecoverable().await; + error!( + "Unrecoverable Error: Failed to read the processed assets at {path:?} in order to remove assets that no longer exist \ + in the source directory. Restart the asset processor to fully reprocess assets. HTTP Status Code {status}" + ); + } AssetReaderError::Io(err) => { self.log_unrecoverable().await; error!( @@ -752,7 +766,7 @@ impl AssetProcessor { .await .map_err(|e| ProcessError::AssetReaderError { path: asset_path.clone(), - err: AssetReaderError::Io(e), + err: AssetReaderError::Io(e.into()), })?; // PERF: in theory these hashes could be streamed if we want to avoid allocating the whole asset. @@ -878,11 +892,11 @@ impl AssetProcessor { state_is_valid = false; }; let Ok(source) = self.get_source(path.source()) else { - (unrecoverable_err)(&"AssetSource does not exist"); + unrecoverable_err(&"AssetSource does not exist"); continue; }; let Ok(processed_writer) = source.processed_writer() else { - (unrecoverable_err)(&"AssetSource does not have a processed AssetWriter registered"); + unrecoverable_err(&"AssetSource does not have a processed AssetWriter registered"); continue; }; @@ -891,7 +905,7 @@ impl AssetProcessor { AssetWriterError::Io(err) => { // any error but NotFound means we could be in a bad state if err.kind() != ErrorKind::NotFound { - (unrecoverable_err)(&err); + unrecoverable_err(&err); } } } @@ -901,7 +915,7 @@ impl AssetProcessor { AssetWriterError::Io(err) => { // any error but NotFound means we could be in a bad state if err.kind() != ErrorKind::NotFound { - (unrecoverable_err)(&err); + unrecoverable_err(&err); } } } diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 352007cb22362..4ba8e3eed34bd 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -61,7 +61,7 @@ impl ReflectAsset { /// Furthermore, this does *not* allow you to have look up two distinct handles, /// you can only have at most one alive at the same time. /// This means that this is *not allowed*: - /// ```rust,no_run + /// ```no_run /// # use bevy_asset::{ReflectAsset, UntypedHandle}; /// # use bevy_ecs::prelude::World; /// # let reflect_asset: ReflectAsset = unimplemented!(); @@ -176,7 +176,7 @@ impl FromType for ReflectAsset { /// the [`ReflectAsset`] type data on the corresponding `T` asset type: /// /// -/// ```rust,no_run +/// ```no_run /// # use bevy_reflect::{TypeRegistry, prelude::*}; /// # use bevy_ecs::prelude::*; /// use bevy_asset::{ReflectHandle, ReflectAsset}; diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index 41bbdf27bf3ec..2165c57b9cb13 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -1,8 +1,8 @@ use crate::{ meta::{AssetHash, MetaTransform}, - Asset, AssetHandleProvider, AssetPath, DependencyLoadState, ErasedLoadedAsset, Handle, - InternalAssetEvent, LoadState, RecursiveDependencyLoadState, StrongHandle, UntypedAssetId, - UntypedHandle, + Asset, AssetHandleProvider, AssetLoadError, AssetPath, DependencyLoadState, ErasedLoadedAsset, + Handle, InternalAssetEvent, LoadState, RecursiveDependencyLoadState, StrongHandle, + UntypedAssetId, UntypedHandle, }; use bevy_ecs::world::World; use bevy_log::warn; @@ -74,6 +74,8 @@ pub(crate) struct AssetInfos { pub(crate) living_labeled_assets: HashMap, HashSet>, pub(crate) handle_providers: HashMap, pub(crate) dependency_loaded_event_sender: HashMap, + pub(crate) dependency_failed_event_sender: + HashMap, AssetLoadError)>, } impl std::fmt::Debug for AssetInfos { @@ -197,7 +199,8 @@ impl AssetInfos { let mut should_load = false; if loading_mode == HandleLoadingMode::Force || (loading_mode == HandleLoadingMode::Request - && info.load_state == LoadState::NotLoaded) + && (info.load_state == LoadState::NotLoaded + || info.load_state == LoadState::Failed)) { info.load_state = LoadState::Loading; info.dep_load_state = DependencyLoadState::Loading; @@ -268,8 +271,12 @@ impl AssetInfos { self.infos.get_mut(&id) } - pub(crate) fn get_path_handle(&self, path: AssetPath) -> Option { - let id = *self.path_to_id.get(&path)?; + pub(crate) fn get_path_id(&self, path: &AssetPath) -> Option { + self.path_to_id.get(path).copied() + } + + pub(crate) fn get_path_handle(&self, path: &AssetPath) -> Option { + let id = *self.path_to_id.get(path)?; self.get_id_handle(id) } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 51d5cc2dc2604..51cb399d10182 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -12,8 +12,9 @@ use crate::{ MetaTransform, Settings, }, path::AssetPath, - Asset, AssetEvent, AssetHandleProvider, AssetId, AssetMetaCheck, Assets, DeserializeMetaError, - ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, UntypedHandle, + Asset, AssetEvent, AssetHandleProvider, AssetId, AssetLoadFailedEvent, AssetMetaCheck, Assets, + DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, + UntypedAssetLoadFailedEvent, UntypedHandle, }; use bevy_ecs::prelude::*; use bevy_log::{error, info, warn}; @@ -119,7 +120,7 @@ impl AssetServer { } } - /// Retrieves the [`AssetReader`] for the given `source`. + /// Retrieves the [`AssetSource`] for the given `source`. pub fn get_source<'a>( &'a self, source: impl Into>, @@ -173,11 +174,30 @@ impl AssetServer { .resource_mut::>>() .send(AssetEvent::LoadedWithDependencies { id: id.typed() }); } - self.data - .infos - .write() + fn failed_sender( + world: &mut World, + id: UntypedAssetId, + path: AssetPath<'static>, + error: AssetLoadError, + ) { + world + .resource_mut::>>() + .send(AssetLoadFailedEvent { + id: id.typed(), + path, + error, + }); + } + + let mut infos = self.data.infos.write(); + + infos .dependency_loaded_event_sender .insert(TypeId::of::(), sender::); + + infos + .dependency_failed_event_sender + .insert(TypeId::of::(), failed_sender::); } pub(crate) fn register_handle_provider(&self, handle_provider: AssetHandleProvider) { @@ -366,6 +386,7 @@ impl AssetServer { let server = self.clone(); IoTaskPool::get() .spawn(async move { + let path_clone = path.clone(); match server.load_untyped_async(path).await { Ok(handle) => server.send_asset_event(InternalAssetEvent::Loaded { id, @@ -377,7 +398,11 @@ impl AssetServer { }), Err(err) => { error!("{err}"); - server.send_asset_event(InternalAssetEvent::Failed { id }); + server.send_asset_event(InternalAssetEvent::Failed { + id, + path: path_clone, + error: err, + }); } } }) @@ -406,7 +431,11 @@ impl AssetServer { // if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if // we cannot find the meta and loader if let Some(handle) = &input_handle { - self.send_asset_event(InternalAssetEvent::Failed { id: handle.id() }); + self.send_asset_event(InternalAssetEvent::Failed { + id: handle.id(), + path: path.clone_owned(), + error: e.clone(), + }); } e })?; @@ -487,9 +516,16 @@ impl AssetServer { match loaded_asset.labeled_assets.get(&label) { Some(labeled_asset) => labeled_asset.handle.clone(), None => { + let mut all_labels: Vec = loaded_asset + .labeled_assets + .keys() + .map(|s| (**s).to_owned()) + .collect(); + all_labels.sort_unstable(); return Err(AssetLoadError::MissingLabel { base_path, label: label.to_string(), + all_labels, }); } } @@ -504,6 +540,8 @@ impl AssetServer { Err(err) => { self.send_asset_event(InternalAssetEvent::Failed { id: base_handle.id(), + error: err.clone(), + path: path.into_owned(), }); Err(err) } @@ -527,7 +565,6 @@ impl AssetServer { IoTaskPool::get() .spawn(async move { if server.data.infos.read().should_reload(&path) { - info!("Reloading {path} because it has changed"); if let Err(err) = server.load_internal(None, path, true, None).await { error!("{}", err); } @@ -683,7 +720,7 @@ impl AssetServer { }), Err(err) => { error!("Failed to load folder. {err}"); - server.send_asset_event(InternalAssetEvent::Failed { id }); + server.send_asset_event(InternalAssetEvent::Failed { id, error: err, path }); }, } }) @@ -768,12 +805,20 @@ impl AssetServer { self.data.infos.read().contains_key(id.into()) } + /// Returns an active untyped asset id for the given path, if the asset at the given path has already started loading, + /// or is still "alive". + pub fn get_path_id<'a>(&self, path: impl Into>) -> Option { + let infos = self.data.infos.read(); + let path = path.into(); + infos.get_path_id(&path) + } + /// Returns an active untyped handle for the given path, if the asset at the given path has already started loading, /// or is still "alive". pub fn get_handle_untyped<'a>(&self, path: impl Into>) -> Option { let infos = self.data.infos.read(); let path = path.into(); - infos.get_path_handle(path) + infos.get_path_handle(&path) } /// Returns the path for the given `id`, if it has one. @@ -867,7 +912,7 @@ impl AssetServer { ron::de::from_bytes(&meta_bytes).map_err(|e| { AssetLoadError::DeserializeMeta { path: asset_path.clone_owned(), - error: Box::new(DeserializeMetaError::DeserializeMinimal(e)), + error: DeserializeMetaError::DeserializeMinimal(e).into(), } })?; let loader_name = match minimal.asset { @@ -887,7 +932,7 @@ impl AssetServer { let meta = loader.deserialize_meta(&meta_bytes).map_err(|e| { AssetLoadError::DeserializeMeta { path: asset_path.clone_owned(), - error: Box::new(e), + error: e.into(), } })?; @@ -924,7 +969,7 @@ impl AssetServer { AssetLoadError::AssetLoaderError { path: asset_path.clone_owned(), loader_name: loader.type_name(), - error: e, + error: e.into(), } }) } @@ -934,6 +979,7 @@ impl AssetServer { pub fn handle_internal_asset_events(world: &mut World) { world.resource_scope(|world, server: Mut| { let mut infos = server.data.infos.write(); + let mut untyped_failures = vec![]; for event in server.data.asset_event_receiver.try_iter() { match event { InternalAssetEvent::Loaded { id, loaded_asset } => { @@ -951,10 +997,30 @@ pub fn handle_internal_asset_events(world: &mut World) { .expect("Asset event sender should exist"); sender(world, id); } - InternalAssetEvent::Failed { id } => infos.process_asset_fail(id), + InternalAssetEvent::Failed { id, path, error } => { + infos.process_asset_fail(id); + + // Send untyped failure event + untyped_failures.push(UntypedAssetLoadFailedEvent { + id, + path: path.clone(), + error: error.clone(), + }); + + // Send typed failure event + let sender = infos + .dependency_failed_event_sender + .get(&id.type_id()) + .expect("Asset failed event sender should exist"); + sender(world, id, path, error); + } } } + if !untyped_failures.is_empty() { + world.send_event_batch(untyped_failures); + } + fn queue_ancestors( asset_path: &AssetPath, infos: &AssetInfos, @@ -974,7 +1040,7 @@ pub fn handle_internal_asset_events(world: &mut World) { current_folder = parent.to_path_buf(); let parent_asset_path = AssetPath::from(current_folder.clone()).with_source(source.clone()); - if let Some(folder_handle) = infos.get_path_handle(parent_asset_path.clone()) { + if let Some(folder_handle) = infos.get_path_handle(&parent_asset_path) { info!("Reloading folder {parent_asset_path} because the content has changed"); server.load_folder_internal(folder_handle.id(), parent_asset_path); } @@ -1025,6 +1091,7 @@ pub fn handle_internal_asset_events(world: &mut World) { } for path in paths_to_reload { + info!("Reloading {path} because it has changed"); server.reload(path); } }); @@ -1059,6 +1126,8 @@ pub(crate) enum InternalAssetEvent { }, Failed { id: UntypedAssetId, + path: AssetPath<'static>, + error: AssetLoadError, }, } @@ -1102,7 +1171,7 @@ pub enum RecursiveDependencyLoadState { } /// An error that occurs during an [`Asset`] load. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] pub enum AssetLoadError { #[error("Requested handle of type {requested:?} for asset '{path}' does not match actual asset type '{actual_asset_name}', which used loader '{loader_name}'")] RequestedHandleTypeMismatch { @@ -1136,24 +1205,29 @@ pub enum AssetLoadError { AssetLoaderError { path: AssetPath<'static>, loader_name: &'static str, - error: Box, + error: Arc, }, - #[error("The file at '{base_path}' does not contain the labeled asset '{label}'.")] + #[error("The file at '{}' does not contain the labeled asset '{}'; it contains the following {} assets: {}", + base_path, + label, + all_labels.len(), + all_labels.iter().map(|l| format!("'{}'", l)).collect::>().join(", "))] MissingLabel { base_path: AssetPath<'static>, label: String, + all_labels: Vec, }, } /// An error that occurs when an [`AssetLoader`] is not registered for a given extension. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("no `AssetLoader` found{}", format_missing_asset_ext(.extensions))] pub struct MissingAssetLoaderForExtensionError { extensions: Vec, } /// An error that occurs when an [`AssetLoader`] is not registered for a given [`std::any::type_name`]. -#[derive(Error, Debug)] +#[derive(Error, Debug, Clone)] #[error("no `AssetLoader` found with the name '{type_name}'")] pub struct MissingAssetLoaderForTypeNameError { type_name: String, diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 56a53ee02b4b5..709d595d4e758 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -1,51 +1,25 @@ use crate::{AudioSource, Decodable}; use bevy_asset::{Asset, Handle}; -use bevy_derive::{Deref, DerefMut}; +use bevy_derive::Deref; use bevy_ecs::prelude::*; use bevy_math::Vec3; use bevy_reflect::prelude::*; -/// Defines the volume to play an audio source at. -#[derive(Clone, Copy, Debug, Reflect)] -pub enum Volume { - /// A volume level relative to the global volume. - Relative(VolumeLevel), - /// A volume level that ignores the global volume. - Absolute(VolumeLevel), -} - -impl Default for Volume { - fn default() -> Self { - Self::Relative(VolumeLevel::default()) - } -} - -impl Volume { - /// Create a new volume level relative to the global volume. - pub fn new_relative(volume: f32) -> Self { - Self::Relative(VolumeLevel::new(volume)) - } - /// Create a new volume level that ignores the global volume. - pub fn new_absolute(volume: f32) -> Self { - Self::Absolute(VolumeLevel::new(volume)) - } -} - /// A volume level equivalent to a non-negative float. -#[derive(Clone, Copy, Deref, DerefMut, Debug, Reflect)] -pub struct VolumeLevel(pub(crate) f32); +#[derive(Clone, Copy, Deref, Debug, Reflect)] +pub struct Volume(pub(crate) f32); -impl Default for VolumeLevel { +impl Default for Volume { fn default() -> Self { Self(1.0) } } -impl VolumeLevel { +impl Volume { /// Create a new volume level. pub fn new(volume: f32) -> Self { debug_assert!(volume >= 0.0); - Self(volume) + Self(f32::max(volume, 0.)) } /// Get the value of the volume level. pub fn get(&self) -> f32 { @@ -53,7 +27,7 @@ impl VolumeLevel { } /// Zero (silent) volume level - pub const ZERO: Self = VolumeLevel(0.0); + pub const ZERO: Self = Volume(0.0); } /// The way Bevy manages the sound playback. @@ -106,7 +80,7 @@ impl PlaybackSettings { /// Will play the associated audio source once. pub const ONCE: PlaybackSettings = PlaybackSettings { mode: PlaybackMode::Once, - volume: Volume::Relative(VolumeLevel(1.0)), + volume: Volume(1.0), speed: 1.0, paused: false, spatial: false, @@ -115,28 +89,19 @@ impl PlaybackSettings { /// Will play the associated audio source in a loop. pub const LOOP: PlaybackSettings = PlaybackSettings { mode: PlaybackMode::Loop, - volume: Volume::Relative(VolumeLevel(1.0)), - speed: 1.0, - paused: false, - spatial: false, + ..PlaybackSettings::ONCE }; /// Will play the associated audio source once and despawn the entity afterwards. pub const DESPAWN: PlaybackSettings = PlaybackSettings { mode: PlaybackMode::Despawn, - volume: Volume::Relative(VolumeLevel(1.0)), - speed: 1.0, - paused: false, - spatial: false, + ..PlaybackSettings::ONCE }; /// Will play the associated audio source once and remove the audio components afterwards. pub const REMOVE: PlaybackSettings = PlaybackSettings { mode: PlaybackMode::Remove, - volume: Volume::Relative(VolumeLevel(1.0)), - speed: 1.0, - paused: false, - spatial: false, + ..PlaybackSettings::ONCE }; /// Helper to start in a paused state. @@ -196,21 +161,21 @@ impl SpatialListener { } } -/// Use this [`Resource`] to control the global volume of all audio with a [`Volume::Relative`] volume. +/// Use this [`Resource`] to control the global volume of all audio. /// /// Note: changing this value will not affect already playing audio. #[derive(Resource, Default, Clone, Copy, Reflect)] #[reflect(Resource)] pub struct GlobalVolume { /// The global volume of all audio. - pub volume: VolumeLevel, + pub volume: Volume, } impl GlobalVolume { /// Create a new [`GlobalVolume`] with the given volume. pub fn new(volume: f32) -> Self { Self { - volume: VolumeLevel::new(volume), + volume: Volume::new(volume), } } } diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index ecbf805bc1c34..2acf0c6ebba18 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -1,6 +1,6 @@ use crate::{ AudioSourceBundle, Decodable, GlobalVolume, PlaybackMode, PlaybackSettings, SpatialAudioSink, - SpatialListener, SpatialScale, Volume, + SpatialListener, SpatialScale, }; use bevy_asset::{Asset, Assets, Handle}; use bevy_ecs::{prelude::*, system::SystemParam}; @@ -77,8 +77,8 @@ impl<'w, 's> EarPositions<'w, 's> { .unwrap_or_else(|| { let settings = SpatialListener::default(); ( - (settings.left_ear_offset * self.scale.0), - (settings.right_ear_offset * self.scale.0), + settings.left_ear_offset * self.scale.0, + settings.right_ear_offset * self.scale.0, ) }); @@ -159,10 +159,7 @@ pub(crate) fn play_queued_audio_system( }; sink.set_speed(settings.speed); - match settings.volume { - Volume::Relative(vol) => sink.set_volume(vol.0 * global_volume.volume.0), - Volume::Absolute(vol) => sink.set_volume(vol.0), - } + sink.set_volume(settings.volume.0 * global_volume.volume.0); if settings.paused { sink.pause(); @@ -202,10 +199,7 @@ pub(crate) fn play_queued_audio_system( }; sink.set_speed(settings.speed); - match settings.volume { - Volume::Relative(vol) => sink.set_volume(vol.0 * global_volume.volume.0), - Volume::Absolute(vol) => sink.set_volume(vol.0), - } + sink.set_volume(settings.volume.0 * global_volume.volume.0); if settings.paused { sink.pause(); @@ -291,7 +285,7 @@ pub(crate) fn audio_output_available(audio_output: Res) -> bool { /// Updates spatial audio sinks when emitter positions change. pub(crate) fn update_emitter_positions( - mut emitters: Query<(&mut GlobalTransform, &SpatialAudioSink), Changed>, + mut emitters: Query<(&GlobalTransform, &SpatialAudioSink), Changed>, spatial_scale: Res, ) { for (transform, sink) in emitters.iter_mut() { diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 30f9c8cf8da9e..8b0c7090eac14 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -97,8 +97,8 @@ pub trait Decodable: Send + Sync + 'static { } impl Decodable for AudioSource { - type Decoder = rodio::Decoder>; type DecoderItem = > as Iterator>::Item; + type Decoder = rodio::Decoder>; fn decoder(&self) -> Self::Decoder { rodio::Decoder::new(Cursor::new(self.clone())).unwrap() diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 2bc1c0ce5a469..1d63ac7d461f8 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -63,7 +63,7 @@ struct AudioPlaySet; /// Insert an [`AudioBundle`] onto your entities to play audio. #[derive(Default)] pub struct AudioPlugin { - /// The global volume for all audio entities with a [`Volume::Relative`] volume. + /// The global volume for all audio entities. pub global_volume: GlobalVolume, /// The scale factor applied to the positions of audio sources and listeners for /// spatial audio. @@ -72,12 +72,11 @@ pub struct AudioPlugin { impl Plugin for AudioPlugin { fn build(&self, app: &mut App) { - app.register_type::() + app.register_type::() .register_type::() .register_type::() .register_type::() .register_type::() - .register_type::() .register_type::() .insert_resource(self.global_volume) .insert_resource(self.spatial_scale) diff --git a/crates/bevy_core/src/name.rs b/crates/bevy_core/src/name.rs index 7cae9c796dabd..dfa4a11c118bd 100644 --- a/crates/bevy_core/src/name.rs +++ b/crates/bevy_core/src/name.rs @@ -10,6 +10,9 @@ use std::{ ops::Deref, }; +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + /// Component used to identify an entity. Stores a hash for faster comparisons. /// /// The hash is eagerly re-computed upon each update to the name. @@ -19,8 +22,9 @@ use std::{ /// used instead as the default unique identifier. #[derive(Reflect, Component, Clone)] #[reflect(Component, Default, Debug)] +#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] pub struct Name { - hash: u64, // TODO: Shouldn't be serialized + hash: u64, // Won't be serialized (see: `bevy_core::serde` module) name: Cow<'static, str>, } @@ -88,7 +92,7 @@ impl std::fmt::Debug for Name { /// Convenient query for giving a human friendly name to an entity. /// -/// ```rust +/// ``` /// # use bevy_core::prelude::*; /// # use bevy_ecs::prelude::*; /// # #[derive(Component)] pub struct Score(f32); diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 9a9777a43f212..91258c034de59 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -20,6 +20,10 @@ 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); + + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.allow_ambiguous_resource::>(); + } } fn finish(&self, app: &mut App) { diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 09f668495d26a..85f3e93ff1be1 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -265,12 +265,7 @@ impl ViewNode for BloomNode { let mut upsampling_final_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("bloom_upsampling_final_pass"), - color_attachments: &[Some(view_target.get_unsampled_color_attachment( - Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }, - ))], + color_attachments: &[Some(view_target.get_unsampled_color_attachment())], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 8d2d3bdd4d275..69c789933c686 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -175,19 +175,19 @@ pub struct BloomPrefilterSettings { pub threshold_softness: f32, } -#[derive(Clone, Reflect, PartialEq, Eq, Hash, Copy)] +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, Copy)] pub enum BloomCompositeMode { EnergyConserving, Additive, } impl ExtractComponent for BloomSettings { - type Data = (&'static Self, &'static Camera); + type QueryData = (&'static Self, &'static Camera); - type Filter = (); + type QueryFilter = (); type Out = (Self, BloomUniforms); - fn extract_component((settings, camera): QueryItem<'_, Self::Data>) -> Option { + fn extract_component((settings, camera): QueryItem<'_, Self::QueryData>) -> Option { match ( camera.physical_viewport_rect(), camera.physical_viewport_size(), diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs index af51d695eafa2..6e512b873572a 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -77,11 +77,11 @@ pub struct CASUniform { } impl ExtractComponent for ContrastAdaptiveSharpeningSettings { - type Data = &'static Self; - type Filter = With; + type QueryData = &'static Self; + type QueryFilter = With; type Out = (DenoiseCAS, CASUniform); - fn extract_component(item: QueryItem) -> Option { + fn extract_component(item: QueryItem) -> Option { if !item.enabled || item.sharpening_strength == 0.0 { return None; } diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index fcd794029b04f..d76c0c315c0b6 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -1,11 +1,11 @@ -use crate::{ - clear_color::ClearColorConfig, - tonemapping::{DebandDither, Tonemapping}, -}; +use crate::tonemapping::{DebandDither, Tonemapping}; use bevy_ecs::prelude::*; use bevy_reflect::Reflect; use bevy_render::{ - camera::{Camera, CameraProjection, CameraRenderGraph, OrthographicProjection}, + camera::{ + Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph, + OrthographicProjection, + }, extract_component::ExtractComponent, primitives::Frustum, view::VisibleEntities, @@ -15,9 +15,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; #[derive(Component, Default, Reflect, Clone, ExtractComponent)] #[extract_component_filter(With)] #[reflect(Component)] -pub struct Camera2d { - pub clear_color: ClearColorConfig, -} +pub struct Camera2d; #[derive(Bundle)] pub struct Camera2dBundle { @@ -31,6 +29,7 @@ pub struct Camera2dBundle { pub camera_2d: Camera2d, pub tonemapping: Tonemapping, pub deband_dither: DebandDither, + pub main_texture_usages: CameraMainTextureUsages, } impl Default for Camera2dBundle { @@ -57,9 +56,10 @@ impl Default for Camera2dBundle { transform, global_transform: Default::default(), camera: Camera::default(), - camera_2d: Camera2d::default(), + camera_2d: Camera2d, tonemapping: Tonemapping::None, deband_dither: DebandDither::Disabled, + main_texture_usages: Default::default(), } } } @@ -95,9 +95,10 @@ impl Camera2dBundle { transform, global_transform: Default::default(), camera: Camera::default(), - camera_2d: Camera2d::default(), + camera_2d: Camera2d, tonemapping: Tonemapping::None, deband_dither: DebandDither::Disabled, + main_texture_usages: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs index c3a19e93df26b..656db89100819 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs @@ -1,13 +1,10 @@ -use crate::{ - clear_color::{ClearColor, ClearColorConfig}, - core_2d::{camera_2d::Camera2d, Transparent2d}, -}; +use crate::core_2d::Transparent2d; use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::RenderPhase, - render_resource::{LoadOp, Operations, RenderPassDescriptor, StoreOp}, + render_resource::RenderPassDescriptor, renderer::RenderContext, view::{ExtractedView, ViewTarget}, }; @@ -20,7 +17,6 @@ pub struct MainPass2dNode { &'static ExtractedCamera, &'static RenderPhase, &'static ViewTarget, - &'static Camera2d, ), With, >, @@ -46,28 +42,19 @@ impl Node for MainPass2dNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); - let Ok((camera, transparent_phase, target, camera_2d)) = - self.query.get_manual(world, view_entity) + let Ok((camera, transparent_phase, target)) = self.query.get_manual(world, view_entity) else { // no target return Ok(()); }; + { #[cfg(feature = "trace")] let _main_pass_2d = info_span!("main_pass_2d").entered(); let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_pass_2d"), - color_attachments: &[Some(target.get_color_attachment(Operations { - load: match camera_2d.clear_color { - ClearColorConfig::Default => { - LoadOp::Clear(world.resource::().0.into()) - } - ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), - ClearColorConfig::None => LoadOp::Load, - }, - store: StoreOp::Store, - }))], + color_attachments: &[Some(target.get_color_attachment())], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, @@ -88,10 +75,7 @@ impl Node for MainPass2dNode { let _reset_viewport_pass_2d = info_span!("reset_viewport_pass_2d").entered(); let pass_descriptor = RenderPassDescriptor { label: Some("reset_viewport_pass_2d"), - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }))], + color_attachments: &[Some(target.get_color_attachment())], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, 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 33579994c9aca..0b8e21da0eaee 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -1,11 +1,8 @@ -use crate::{ - clear_color::ClearColorConfig, - tonemapping::{DebandDither, Tonemapping}, -}; +use crate::tonemapping::{DebandDither, Tonemapping}; use bevy_ecs::prelude::*; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_render::{ - camera::{Camera, CameraRenderGraph, Projection}, + camera::{Camera, CameraMainTextureUsages, CameraRenderGraph, Projection}, extract_component::ExtractComponent, primitives::Frustum, render_resource::{LoadOp, TextureUsages}, @@ -19,8 +16,6 @@ use serde::{Deserialize, Serialize}; #[extract_component_filter(With)] #[reflect(Component)] pub struct Camera3d { - /// The clear color operation to perform for the main 3d pass. - pub clear_color: ClearColorConfig, /// The depth clear operation to perform for the main 3d pass. pub depth_load_op: Camera3dDepthLoadOp, /// The texture usages for the depth texture created for the main 3d pass. @@ -37,7 +32,7 @@ pub struct Camera3d { /// regardless of this setting. /// - Setting this to `0` disables the screen-space refraction effect entirely, and falls /// back to refracting only the environment map light's texture. - /// - If set to more than `0`, any opaque [`clear_color`](Camera3d::clear_color) will obscure the environment + /// - If set to more than `0`, any opaque [`clear_color`](Camera::clear_color) will obscure the environment /// map light's texture, preventing it from being visible “through” transmissive materials. If you'd like /// to still have the environment map show up in your refractions, you can set the clear color's alpha to `0.0`. /// Keep in mind that depending on the platform and your window settings, this may cause the window to become @@ -55,7 +50,6 @@ pub struct Camera3d { impl Default for Camera3d { fn default() -> Self { Self { - clear_color: ClearColorConfig::Default, depth_load_op: Default::default(), depth_texture_usages: TextureUsages::RENDER_ATTACHMENT.into(), screen_space_specular_transmission_steps: 1, @@ -64,7 +58,8 @@ impl Default for Camera3d { } } -#[derive(Clone, Copy, Reflect)] +#[derive(Clone, Copy, Reflect, Serialize, Deserialize)] +#[reflect(Serialize, Deserialize)] pub struct Camera3dDepthTextureUsage(u32); impl From for Camera3dDepthTextureUsage { @@ -148,6 +143,7 @@ pub struct Camera3dBundle { pub tonemapping: Tonemapping, pub dither: DebandDither, pub color_grading: ColorGrading, + pub main_texture_usages: CameraMainTextureUsages, } // NOTE: ideally Perspective and Orthographic defaults can share the same impl, but sadly it breaks rust's type inference @@ -165,6 +161,7 @@ impl Default for Camera3dBundle { tonemapping: Default::default(), dither: DebandDither::Enabled, color_grading: ColorGrading::default(), + main_texture_usages: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 2de30a934af64..804f6afcf8e18 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 @@ -1,25 +1,20 @@ use crate::{ - clear_color::{ClearColor, ClearColorConfig}, - core_3d::{Camera3d, Opaque3d}, - prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + core_3d::Opaque3d, skybox::{SkyboxBindGroup, SkyboxPipelineId}, }; -use bevy_ecs::{prelude::*, query::QueryItem}; +use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::RenderPhase, - render_resource::{ - LoadOp, Operations, PipelineCache, RenderPassDepthStencilAttachment, RenderPassDescriptor, - StoreOp, - }, + render_resource::{PipelineCache, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget, ViewUniformOffset}, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -use super::{AlphaMask3d, Camera3dDepthLoadOp}; +use super::AlphaMask3d; /// A [`bevy_render::render_graph::Node`] that runs the [`Opaque3d`] and [`AlphaMask3d`] [`RenderPhase`]. #[derive(Default)] @@ -29,13 +24,8 @@ impl ViewNode for MainOpaquePass3dNode { &'static ExtractedCamera, &'static RenderPhase, &'static RenderPhase, - &'static Camera3d, &'static ViewTarget, &'static ViewDepthTexture, - Option<&'static DepthPrepass>, - Option<&'static NormalPrepass>, - Option<&'static MotionVectorPrepass>, - Option<&'static DeferredPrepass>, Option<&'static SkyboxPipelineId>, Option<&'static SkyboxBindGroup>, &'static ViewUniformOffset, @@ -49,30 +39,14 @@ impl ViewNode for MainOpaquePass3dNode { camera, opaque_phase, alpha_mask_phase, - camera_3d, target, depth, - depth_prepass, - normal_prepass, - motion_vector_prepass, - deferred_prepass, skybox_pipeline, skybox_bind_group, view_uniform_offset, ): QueryItem, world: &World, ) -> Result<(), NodeRunError> { - let load = if deferred_prepass.is_none() { - match camera_3d.clear_color { - ClearColorConfig::Default => LoadOp::Clear(world.resource::().0.into()), - ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), - ClearColorConfig::None => LoadOp::Load, - } - } else { - // If the deferred lighting pass has run, don't clear again in this pass. - LoadOp::Load - }; - // Run the opaque pass, sorted front-to-back // NOTE: Scoped to drop the mutable borrow of render_context #[cfg(feature = "trace")] @@ -81,33 +55,8 @@ impl ViewNode for MainOpaquePass3dNode { // Setup render pass let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_opaque_pass_3d"), - // NOTE: The opaque pass loads the color - // buffer as well as writing to it. - color_attachments: &[Some(target.get_color_attachment(Operations { - load, - store: StoreOp::Store, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it - depth_ops: Some(Operations { - load: if depth_prepass.is_some() - || normal_prepass.is_some() - || motion_vector_prepass.is_some() - || deferred_prepass.is_some() - { - // if any prepass runs, it will generate a depth buffer so we should use it, - // even if only the normal_prepass is used. - Camera3dDepthLoadOp::Load - } else { - // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. - camera_3d.depth_load_op.clone() - } - .into(), - store: StoreOp::Store, - }), - stencil_ops: None, - }), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), timestamp_writes: None, occlusion_query_set: None, }); @@ -127,13 +76,17 @@ impl ViewNode for MainOpaquePass3dNode { } // Draw the skybox using a fullscreen triangle - if let (Some(skybox_pipeline), Some(skybox_bind_group)) = + if let (Some(skybox_pipeline), Some(SkyboxBindGroup(skybox_bind_group))) = (skybox_pipeline, skybox_bind_group) { let pipeline_cache = world.resource::(); if let Some(pipeline) = pipeline_cache.get_render_pipeline(skybox_pipeline.0) { render_pass.set_render_pipeline(pipeline); - render_pass.set_bind_group(0, &skybox_bind_group.0, &[view_uniform_offset.offset]); + render_pass.set_bind_group( + 0, + &skybox_bind_group.0, + &[view_uniform_offset.offset, skybox_bind_group.1], + ); render_pass.draw(0..3, 0..1); } } diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs index fbd290a4fea57..73a679ba047eb 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -5,10 +5,7 @@ use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::RenderPhase, - render_resource::{ - Extent3d, LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor, - StoreOp, - }, + render_resource::{Extent3d, RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget}, }; @@ -45,20 +42,8 @@ impl ViewNode for MainTransmissivePass3dNode { let render_pass_descriptor = RenderPassDescriptor { label: Some("main_transmissive_pass_3d"), - // NOTE: The transmissive pass loads the color buffer as well as overwriting it where appropriate. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // NOTE: The transmissive main pass loads the depth buffer and possibly overwrites it - depth_ops: Some(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }), - stencil_ops: None, - }), + color_attachments: &[Some(target.get_color_attachment())], + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), timestamp_writes: None, occlusion_query_set: None, }; diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs index 22bc3f5e91abc..1ffb059007c8f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transparent_pass_3d_node.rs @@ -4,9 +4,7 @@ use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::RenderPhase, - render_resource::{ - LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor, StoreOp, - }, + render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, view::{ViewDepthTexture, ViewTarget}, }; @@ -41,25 +39,14 @@ impl ViewNode for MainTransparentPass3dNode { let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_transparent_pass_3d"), - // NOTE: The transparent pass loads the color buffer as well as overwriting it where appropriate. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // NOTE: For the transparent pass we load the depth buffer. There should be no - // need to write to it, but store is set to `true` as a workaround for issue #3776, - // https://github.com/bevyengine/bevy/issues/3776 - // so that wgpu does not clear the depth buffer. - // As the opaque and alpha mask passes run first, opaque meshes can occlude - // transparent ones. - depth_ops: Some(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }), - stencil_ops: None, - }), + color_attachments: &[Some(target.get_color_attachment())], + // NOTE: For the transparent pass we load the depth buffer. There should be no + // need to write to it, but store is set to `true` as a workaround for issue #3776, + // https://github.com/bevyengine/bevy/issues/3776 + // so that wgpu does not clear the depth buffer. + // As the opaque and alpha mask passes run first, opaque meshes can occlude + // transparent ones. + depth_stencil_attachment: Some(depth.get_attachment(StoreOp::Store)), timestamp_writes: None, occlusion_query_set: None, }); @@ -79,10 +66,7 @@ impl ViewNode for MainTransparentPass3dNode { let _reset_viewport_pass_3d = info_span!("reset_viewport_pass_3d").entered(); let pass_descriptor = RenderPassDescriptor { label: Some("reset_viewport_pass_3d"), - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Load, - store: StoreOp::Store, - }))], + color_attachments: &[Some(target.get_color_attachment())], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 1a38e5eb932c5..5d6e72e88953e 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -42,6 +42,7 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::prelude::*; use bevy_render::{ camera::{Camera, ExtractedCamera}, + color::Color, extract_component::ExtractComponentPlugin, prelude::Msaa, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, @@ -54,7 +55,7 @@ use bevy_render::{ TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, }, renderer::RenderDevice, - texture::{BevyDefault, TextureCache}, + texture::{BevyDefault, ColorAttachment, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -83,6 +84,8 @@ impl Plugin for Core3dPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() + .register_type::() + .register_type::() .add_plugins((SkyboxPlugin, ExtractComponentPlugin::::default())) .add_systems(PostUpdate, check_msaa); @@ -524,7 +527,7 @@ pub fn prepare_core_3d_depth_textures( } let mut textures = HashMap::default(); - for (entity, camera, _, _) in &views_3d { + for (entity, camera, _, camera_3d) in &views_3d { let Some(physical_target_size) = camera.physical_target_size else { continue; }; @@ -558,10 +561,13 @@ pub fn prepare_core_3d_depth_textures( }) .clone(); - commands.entity(entity).insert(ViewDepthTexture { - texture: cached_texture.texture, - view: cached_texture.default_view, - }); + commands.entity(entity).insert(ViewDepthTexture::new( + cached_texture, + match camera_3d.depth_load_op { + Camera3dDepthLoadOp::Clear(v) => Some(v), + Camera3dDepthLoadOp::Load => None, + }, + )); } } @@ -822,10 +828,14 @@ pub fn prepare_prepass_textures( }); commands.entity(entity).insert(ViewPrepassTextures { - depth: cached_depth_texture, - normal: cached_normals_texture, - motion_vectors: cached_motion_vectors_texture, - deferred: cached_deferred_texture, + depth: cached_depth_texture.map(|t| ColorAttachment::new(t, None, Color::BLACK)), + normal: cached_normals_texture.map(|t| ColorAttachment::new(t, None, Color::BLACK)), + // Red and Green channels are X and Y components of the motion vectors + // Blue channel doesn't matter, but set to 0.0 for possible faster clear + // https://gpuopen.com/performance/#clears + motion_vectors: cached_motion_vectors_texture + .map(|t| ColorAttachment::new(t, None, Color::BLACK)), + deferred: cached_deferred_texture.map(|t| ColorAttachment::new(t, None, Color::BLACK)), deferred_lighting_pass_id: deferred_lighting_pass_id_texture, size, }); diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index 26106016bb21c..e685090437ca1 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -8,18 +8,14 @@ use bevy_render::{ prelude::Color, render_graph::{NodeRunError, RenderGraphContext}, render_phase::RenderPhase, - render_resource::{ - LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, - RenderPassDescriptor, - }, + render_resource::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor}, renderer::RenderContext, view::ViewDepthTexture, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -use crate::core_3d::{Camera3d, Camera3dDepthLoadOp}; -use crate::prepass::{DepthPrepass, MotionVectorPrepass, NormalPrepass, ViewPrepassTextures}; +use crate::prepass::ViewPrepassTextures; use super::{AlphaMask3dDeferred, Opaque3dDeferred}; @@ -36,10 +32,6 @@ impl ViewNode for DeferredGBufferPrepassNode { &'static RenderPhase, &'static ViewDepthTexture, &'static ViewPrepassTextures, - &'static Camera3d, - Option<&'static DepthPrepass>, - Option<&'static NormalPrepass>, - Option<&'static MotionVectorPrepass>, ); fn run( @@ -52,10 +44,6 @@ impl ViewNode for DeferredGBufferPrepassNode { alpha_mask_deferred_phase, view_depth_texture, view_prepass_textures, - camera_3d, - depth_prepass, - normal_prepass, - motion_vector_prepass, ): QueryItem, world: &World, ) -> Result<(), NodeRunError> { @@ -66,37 +54,14 @@ impl ViewNode for DeferredGBufferPrepassNode { view_prepass_textures .normal .as_ref() - .map(|view_normals_texture| RenderPassColorAttachment { - view: &view_normals_texture.default_view, - resolve_target: None, - ops: Operations { - load: if normal_prepass.is_some() { - // Load if the normal_prepass has already run. - // The prepass will have already cleared this for the current frame. - LoadOp::Load - } else { - LoadOp::Clear(Color::BLACK.into()) - }, - store: StoreOp::Store, - }, - }), + .map(|normals_texture| normals_texture.get_attachment()), + ); + color_attachments.push( + view_prepass_textures + .motion_vectors + .as_ref() + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), ); - color_attachments.push(view_prepass_textures.motion_vectors.as_ref().map( - |view_motion_vectors_texture| RenderPassColorAttachment { - view: &view_motion_vectors_texture.default_view, - resolve_target: None, - ops: Operations { - load: if motion_vector_prepass.is_some() { - // Load if the motion_vector_prepass has already run. - // The prepass will have already cleared this for the current frame. - LoadOp::Load - } else { - LoadOp::Clear(Color::BLACK.into()) - }, - store: StoreOp::Store, - }, - }, - )); // If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors: // Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format. @@ -106,7 +71,7 @@ impl ViewNode for DeferredGBufferPrepassNode { #[cfg(all(feature = "webgl", target_arch = "wasm32"))] if let Some(deferred_texture) = &view_prepass_textures.deferred { render_context.command_encoder().clear_texture( - &deferred_texture.texture, + &deferred_texture.texture.texture, &bevy_render::render_resource::ImageSubresourceRange::default(), ); } @@ -115,16 +80,20 @@ impl ViewNode for DeferredGBufferPrepassNode { view_prepass_textures .deferred .as_ref() - .map(|deferred_texture| RenderPassColorAttachment { - view: &deferred_texture.default_view, - resolve_target: None, - ops: Operations { - #[cfg(all(feature = "webgl", target_arch = "wasm32"))] - load: LoadOp::Load, - #[cfg(not(all(feature = "webgl", target_arch = "wasm32")))] - load: LoadOp::Clear(Default::default()), - store: StoreOp::Store, - }, + .map(|deferred_texture| { + #[cfg(all(feature = "webgl", target_arch = "wasm32"))] + { + RenderPassColorAttachment { + view: &deferred_texture.texture.default_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + } + } + #[cfg(not(all(feature = "webgl", target_arch = "wasm32")))] + deferred_texture.get_attachment() }), ); @@ -136,7 +105,7 @@ impl ViewNode for DeferredGBufferPrepassNode { view: &deferred_lighting_pass_id.default_view, resolve_target: None, ops: Operations { - load: LoadOp::Clear(Default::default()), + load: LoadOp::Clear(Color::BLACK.into()), store: StoreOp::Store, }, }), @@ -152,24 +121,7 @@ impl ViewNode for DeferredGBufferPrepassNode { let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("deferred"), color_attachments: &color_attachments, - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &view_depth_texture.view, - depth_ops: Some(Operations { - load: if depth_prepass.is_some() - || normal_prepass.is_some() - || motion_vector_prepass.is_some() - { - // If any prepass runs, it will generate a depth buffer so we should use it. - Camera3dDepthLoadOp::Load - } else { - // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. - camera_3d.depth_load_op.clone() - } - .into(), - store: StoreOp::Store, - }), - stencil_ops: None, - }), + depth_stencil_attachment: Some(view_depth_texture.get_attachment(StoreOp::Store)), timestamp_writes: None, occlusion_query_set: None, }); @@ -198,7 +150,7 @@ impl ViewNode for DeferredGBufferPrepassNode { // Copy depth buffer to texture. render_context.command_encoder().copy_texture_to_texture( view_depth_texture.texture.as_image_copy(), - prepass_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.texture.as_image_copy(), view_prepass_textures.size, ); } diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index f710b8c704650..5aad6703eda56 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -1,6 +1,5 @@ pub mod blit; pub mod bloom; -pub mod clear_color; pub mod contrast_adaptive_sharpening; pub mod core_2d; pub mod core_3d; @@ -29,7 +28,6 @@ pub mod experimental { pub mod prelude { #[doc(hidden)] pub use crate::{ - clear_color::ClearColor, core_2d::{Camera2d, Camera2dBundle}, core_3d::{Camera3d, Camera3dBundle}, }; @@ -38,7 +36,6 @@ pub mod prelude { use crate::{ blit::BlitPlugin, bloom::BloomPlugin, - clear_color::{ClearColor, ClearColorConfig}, contrast_adaptive_sharpening::CASPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin, @@ -46,13 +43,13 @@ use crate::{ fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, fxaa::FxaaPlugin, msaa_writeback::MsaaWritebackPlugin, - prepass::{DepthPrepass, NormalPrepass}, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, }; use bevy_app::{App, Plugin}; use bevy_asset::load_internal_asset; -use bevy_render::{extract_resource::ExtractResourcePlugin, prelude::Shader}; +use bevy_render::prelude::Shader; #[derive(Default)] pub struct CorePipelinePlugin; @@ -66,13 +63,11 @@ impl Plugin for CorePipelinePlugin { Shader::from_wgsl ); - app.register_type::() - .register_type::() - .register_type::() + app.register_type::() .register_type::() - .init_resource::() + .register_type::() + .register_type::() .add_plugins(( - ExtractResourcePlugin::::default(), Core2dPlugin, Core3dPlugin, CopyDeferredLightingIdPlugin, diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 096936800c88d..06f294dae0e7f 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -7,6 +7,7 @@ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, + color::Color, render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, render_resource::BindGroupEntries, renderer::RenderContext, @@ -60,12 +61,17 @@ impl Node for MsaaWritebackNode { fn update(&mut self, world: &mut World) { self.cameras.update_archetypes(world); } + fn run( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { + if *world.resource::() == Msaa::Off { + return Ok(()); + } + let view_entity = graph.view_entity(); if let Ok((target, blit_pipeline_id)) = self.cameras.get_manual(world, view_entity) { let blit_pipeline = world.resource::(); @@ -81,13 +87,18 @@ impl Node for MsaaWritebackNode { let pass_descriptor = RenderPassDescriptor { label: Some("msaa_writeback"), - // The target's "resolve target" is the "destination" in post_process + // The target's "resolve target" is the "destination" in post_process. // We will indirectly write the results to the "destination" using // the MSAA resolve step. - color_attachments: &[Some(target.get_color_attachment(Operations { - load: LoadOp::Clear(Default::default()), - store: StoreOp::Store, - }))], + color_attachments: &[Some(RenderPassColorAttachment { + // If MSAA is enabled, then the sampled texture will always exist + view: target.sampled_main_texture_view().unwrap(), + resolve_target: Some(post_process.destination), + ops: Operations { + load: LoadOp::Clear(Color::BLACK.into()), + store: StoreOp::Store, + }, + })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, @@ -107,6 +118,7 @@ impl Node for MsaaWritebackNode { render_pass.set_bind_group(0, &bind_group, &[]); render_pass.draw(0..3, 0..1); } + Ok(()) } } diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index 63b8c764af5ff..5006c2a3055c9 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -33,8 +33,8 @@ use bevy_ecs::prelude::*; use bevy_reflect::Reflect; use bevy_render::{ render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, - render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat}, - texture::CachedTexture, + render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat, TextureView}, + texture::{CachedTexture, ColorAttachment}, }; use bevy_utils::{nonmax::NonMaxU32, FloatOrd}; @@ -66,16 +66,16 @@ pub struct DeferredPrepass; pub struct ViewPrepassTextures { /// The depth texture generated by the prepass. /// Exists only if [`DepthPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget) - pub depth: Option, + pub depth: Option, /// The normals texture generated by the prepass. /// Exists only if [`NormalPrepass`] is added to the [`ViewTarget`](bevy_render::view::ViewTarget) - pub normal: Option, + pub normal: Option, /// The motion vectors texture generated by the prepass. /// Exists only if [`MotionVectorPrepass`] is added to the `ViewTarget` - pub motion_vectors: Option, + pub motion_vectors: Option, /// The deferred gbuffer generated by the deferred pass. /// Exists only if [`DeferredPrepass`] is added to the `ViewTarget` - pub deferred: Option, + pub deferred: Option, /// A texture that specifies the deferred lighting pass id for a material. /// Exists only if [`DeferredPrepass`] is added to the `ViewTarget` pub deferred_lighting_pass_id: Option, @@ -83,6 +83,26 @@ pub struct ViewPrepassTextures { pub size: Extent3d, } +impl ViewPrepassTextures { + pub fn depth_view(&self) -> Option<&TextureView> { + self.depth.as_ref().map(|t| &t.texture.default_view) + } + + pub fn normal_view(&self) -> Option<&TextureView> { + self.normal.as_ref().map(|t| &t.texture.default_view) + } + + pub fn motion_vectors_view(&self) -> Option<&TextureView> { + self.motion_vectors + .as_ref() + .map(|t| &t.texture.default_view) + } + + pub fn deferred_view(&self) -> Option<&TextureView> { + self.deferred.as_ref().map(|t| &t.texture.default_view) + } +} + /// Opaque phase of the 3D prepass. /// /// Sorted front-to-back by the z-distance in front of the camera. diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 43d6e9f7af031..baaed25bd2871 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -4,13 +4,9 @@ use bevy_render::render_graph::ViewNode; use bevy_render::render_resource::StoreOp; use bevy_render::{ camera::ExtractedCamera, - prelude::Color, render_graph::{NodeRunError, RenderGraphContext}, render_phase::RenderPhase, - render_resource::{ - LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, - RenderPassDescriptor, - }, + render_resource::RenderPassDescriptor, renderer::RenderContext, view::ViewDepthTexture, }; @@ -55,28 +51,11 @@ impl ViewNode for PrepassNode { view_prepass_textures .normal .as_ref() - .map(|view_normals_texture| RenderPassColorAttachment { - view: &view_normals_texture.default_view, - resolve_target: None, - ops: Operations { - load: LoadOp::Clear(Color::BLACK.into()), - store: StoreOp::Store, - }, - }), + .map(|normals_texture| normals_texture.get_attachment()), view_prepass_textures .motion_vectors .as_ref() - .map(|view_motion_vectors_texture| RenderPassColorAttachment { - view: &view_motion_vectors_texture.default_view, - resolve_target: None, - ops: Operations { - // Red and Green channels are X and Y components of the motion vectors - // Blue channel doesn't matter, but set to 0.0 for possible faster clear - // https://gpuopen.com/performance/#clears - load: LoadOp::Clear(Color::BLACK.into()), - store: StoreOp::Store, - }, - }), + .map(|motion_vectors_texture| motion_vectors_texture.get_attachment()), // Use None in place of Deferred attachments None, None, @@ -92,14 +71,7 @@ impl ViewNode for PrepassNode { let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("prepass"), color_attachments: &color_attachments, - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &view_depth_texture.view, - depth_ops: Some(Operations { - load: LoadOp::Clear(0.0), - store: StoreOp::Store, - }), - stencil_ops: None, - }), + depth_stencil_attachment: Some(view_depth_texture.get_attachment(StoreOp::Store)), timestamp_writes: None, occlusion_query_set: None, }); @@ -128,7 +100,7 @@ impl ViewNode for PrepassNode { // Copy depth buffer to texture render_context.command_encoder().copy_texture_to_texture( view_depth_texture.texture.as_image_copy(), - prepass_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.texture.as_image_copy(), view_prepass_textures.size, ); } diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 2d31cea138732..ad0794f4ee568 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -2,21 +2,20 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, - query::With, + query::{QueryItem, With}, schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, }; use bevy_render::{ - extract_component::{ExtractComponent, ExtractComponentPlugin}, + camera::ExposureSettings, + extract_component::{ + ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, + UniformComponentPlugin, + }, render_asset::RenderAssets, render_resource::{ binding_types::{sampler, texture_cube, uniform_buffer}, - BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, - CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, - DepthStencilState, FragmentState, MultisampleState, PipelineCache, PrimitiveState, - RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, - SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState, - TextureFormat, TextureSampleType, VertexState, + *, }, renderer::RenderDevice, texture::{BevyDefault, Image}, @@ -34,7 +33,10 @@ impl Plugin for SkyboxPlugin { fn build(&self, app: &mut App) { load_internal_asset!(app, SKYBOX_SHADER_HANDLE, "skybox.wgsl", Shader::from_wgsl); - app.add_plugins(ExtractComponentPlugin::::default()); + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -68,8 +70,41 @@ impl Plugin for SkyboxPlugin { /// To do so, use `EnvironmentMapLight` alongside this component. /// /// See also . -#[derive(Component, ExtractComponent, Clone)] -pub struct Skybox(pub Handle); +#[derive(Component, Clone)] +pub struct Skybox { + pub image: Handle, + /// Scale factor applied to the skybox image. + /// After applying this multiplier to the image samples, the resulting values should + /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). + pub brightness: f32, +} + +impl ExtractComponent for Skybox { + type QueryData = (&'static Self, Option<&'static ExposureSettings>); + type QueryFilter = (); + type Out = (Self, SkyboxUniforms); + + fn extract_component( + (skybox, exposure_settings): QueryItem<'_, Self::QueryData>, + ) -> Option { + let exposure = exposure_settings + .map(|e| e.exposure()) + .unwrap_or_else(|| ExposureSettings::default().exposure()); + + Some(( + skybox.clone(), + SkyboxUniforms { + brightness: skybox.brightness * exposure, + }, + )) + } +} + +// TODO: Replace with a push constant once WebGPU gets support for that +#[derive(Component, ShaderType, Clone)] +pub struct SkyboxUniforms { + brightness: f32, +} #[derive(Resource)] struct SkyboxPipeline { @@ -88,6 +123,7 @@ impl SkyboxPipeline { sampler(SamplerBindingType::Filtering), uniform_buffer::(true) .visibility(ShaderStages::VERTEX_FRAGMENT), + uniform_buffer::(true), ), ), ), @@ -186,20 +222,23 @@ fn prepare_skybox_pipelines( } #[derive(Component)] -pub struct SkyboxBindGroup(pub BindGroup); +pub struct SkyboxBindGroup(pub (BindGroup, u32)); fn prepare_skybox_bind_groups( mut commands: Commands, pipeline: Res, view_uniforms: Res, + skybox_uniforms: Res>, images: Res>, render_device: Res, - views: Query<(Entity, &Skybox)>, + views: Query<(Entity, &Skybox, &DynamicUniformIndex)>, ) { - for (entity, skybox) in &views { - if let (Some(skybox), Some(view_uniforms)) = - (images.get(&skybox.0), view_uniforms.uniforms.binding()) - { + for (entity, skybox, skybox_uniform_index) in &views { + if let (Some(skybox), Some(view_uniforms), Some(skybox_uniforms)) = ( + images.get(&skybox.image), + view_uniforms.uniforms.binding(), + skybox_uniforms.binding(), + ) { let bind_group = render_device.create_bind_group( "skybox_bind_group", &pipeline.bind_group_layout, @@ -207,10 +246,13 @@ fn prepare_skybox_bind_groups( &skybox.texture_view, &skybox.sampler, view_uniforms, + skybox_uniforms, )), ); - commands.entity(entity).insert(SkyboxBindGroup(bind_group)); + commands + .entity(entity) + .insert(SkyboxBindGroup((bind_group, skybox_uniform_index.index()))); } } } diff --git a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl index 7da40da7937d4..cfbacf0e63ec5 100644 --- a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl +++ b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl @@ -4,6 +4,7 @@ @group(0) @binding(0) var skybox: texture_cube; @group(0) @binding(1) var skybox_sampler: sampler; @group(0) @binding(2) var view: View; +@group(0) @binding(3) var brightness: f32; fn coords_to_ray_direction(position: vec2, viewport: vec4) -> vec3 { // Using world positions of the fragment and camera to calculate a ray direction @@ -62,5 +63,5 @@ fn skybox_fragment(in: VertexOutput) -> @location(0) vec4 { let ray_direction = coords_to_ray_direction(in.position.xy, view.viewport); // Cube maps are left-handed so we negate the z coordinate. - return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)); + return textureSample(skybox, skybox_sampler, ray_direction * vec3(1.0, 1.0, -1.0)) * brightness; } diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 28d7fb42c998d..b0eb4f3057e69 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -206,8 +206,8 @@ impl ViewNode for TemporalAntiAliasNode { &BindGroupEntries::sequential(( view_target.source, &taa_history_textures.read.default_view, - &prepass_motion_vectors_texture.default_view, - &prepass_depth_texture.default_view, + &prepass_motion_vectors_texture.texture.default_view, + &prepass_depth_texture.texture.default_view, &pipelines.nearest_sampler, &pipelines.linear_sampler, )), diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 20c3b87bb6d88..8e277a2e50c59 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -6,7 +6,7 @@ use bevy_reflect::Reflect; use bevy_render::camera::Camera; use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin}; use bevy_render::extract_resource::{ExtractResource, ExtractResourcePlugin}; -use bevy_render::render_asset::RenderAssets; +use bevy_render::render_asset::{RenderAssetPersistencePolicy, RenderAssets}; use bevy_render::render_resource::binding_types::{ sampler, texture_2d, texture_3d, uniform_buffer, }; @@ -356,6 +356,7 @@ fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { CompressedImageFormats::NONE, false, image_sampler, + RenderAssetPersistencePolicy::Unload, ) .unwrap() } @@ -381,5 +382,6 @@ pub fn lut_placeholder() -> Image { }, sampler: ImageSampler::Default, texture_view_descriptor: None, + cpu_persistent_access: RenderAssetPersistencePolicy::Unload, } } diff --git a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl index 494d86900def7..2426f1dce215f 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl +++ b/crates/bevy_core_pipeline/src/tonemapping/tonemapping_shared.wgsl @@ -7,8 +7,8 @@ @group(0) @binding(3) var dt_lut_texture: texture_3d; @group(0) @binding(4) var dt_lut_sampler: sampler; #else - @group(0) @binding(15) var dt_lut_texture: texture_3d; - @group(0) @binding(16) var dt_lut_sampler: sampler; + @group(0) @binding(16) var dt_lut_texture: texture_3d; + @group(0) @binding(17) var dt_lut_sampler: sampler; #endif fn sample_current_lut(p: vec3) -> vec3 { diff --git a/crates/bevy_derive/src/bevy_main.rs b/crates/bevy_derive/src/bevy_main.rs index ac7d6a5e9d1a8..d4504331a6467 100644 --- a/crates/bevy_derive/src/bevy_main.rs +++ b/crates/bevy_derive/src/bevy_main.rs @@ -12,7 +12,7 @@ pub fn bevy_main(_attr: TokenStream, item: TokenStream) -> TokenStream { TokenStream::from(quote! { #[no_mangle] #[cfg(target_os = "android")] - fn android_main(android_app: bevy::winit::AndroidApp) { + fn android_main(android_app: bevy::winit::android_activity::AndroidApp) { let _ = bevy::winit::ANDROID_APP.set(android_app); main(); } diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index dec3647ce9b6c..e82d08f22910b 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -21,16 +21,18 @@ bevy_log = { path = "../bevy_log", version = "0.12.0" } bevy_time = { path = "../bevy_time", version = "0.12.0" } bevy_utils = { path = "../bevy_utils", version = "0.12.0" } +const-fnv1a-hash = "1.1.0" + # MacOS [target.'cfg(all(target_os="macos"))'.dependencies] # Some features of sysinfo are not supported by apple. This will disable those features on apple devices -sysinfo = { version = "0.29.0", default-features = false, features = [ +sysinfo = { version = "0.30.0", default-features = false, features = [ "apple-app-store", ] } # Only include when not bevy_dynamic_plugin and on linux/windows/android [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android"))'.dependencies] -sysinfo = { version = "0.29.0", default-features = false } +sysinfo = { version = "0.30.0", default-features = false } [lints] workspace = true diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index 70b8362b152f5..f3b0bb04c0934 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -1,24 +1,108 @@ +use std::hash::{Hash, Hasher}; +use std::{borrow::Cow, collections::VecDeque}; + use bevy_app::App; use bevy_ecs::system::{Deferred, Res, Resource, SystemBuffer, SystemParam}; -use bevy_log::warn; -use bevy_utils::{Duration, Instant, StableHashMap, Uuid}; -use std::{borrow::Cow, collections::VecDeque}; +use bevy_utils::{hashbrown::HashMap, Duration, Instant, PassHash}; +use const_fnv1a_hash::fnv1a_hash_str_64; + +use crate::DEFAULT_MAX_HISTORY_LENGTH; + +/// Unique diagnostic path, separated by `/`. +/// +/// Requirements: +/// - Can't be empty +/// - Can't have leading or trailing `/` +/// - Can't have empty components. +#[derive(Debug, Clone)] +pub struct DiagnosticPath { + path: Cow<'static, str>, + hash: u64, +} + +impl DiagnosticPath { + /// Create a new `DiagnosticPath`. Usable in const contexts. + /// + /// **Note**: path is not validated, so make sure it follows all the requirements. + pub const fn const_new(path: &'static str) -> DiagnosticPath { + DiagnosticPath { + path: Cow::Borrowed(path), + hash: fnv1a_hash_str_64(path), + } + } + + /// Create a new `DiagnosticPath` from the specified string. + pub fn new(path: impl Into>) -> DiagnosticPath { + let path = path.into(); + + debug_assert!(!path.is_empty(), "diagnostic path can't be empty"); + debug_assert!( + !path.starts_with('/'), + "diagnostic path can't be start with `/`" + ); + debug_assert!( + !path.ends_with('/'), + "diagnostic path can't be end with `/`" + ); + debug_assert!( + !path.contains("//"), + "diagnostic path can't contain empty components" + ); + + DiagnosticPath { + hash: fnv1a_hash_str_64(&path), + path, + } + } -use crate::MAX_DIAGNOSTIC_NAME_WIDTH; + /// Create a new `DiagnosticPath` from an iterator over components. + pub fn from_components<'a>(components: impl IntoIterator) -> DiagnosticPath { + let mut buf = String::new(); -/// Unique identifier for a [`Diagnostic`]. -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] -pub struct DiagnosticId(pub Uuid); + for (i, component) in components.into_iter().enumerate() { + if i > 0 { + buf.push('/'); + } + buf.push_str(component); + } -impl DiagnosticId { - pub const fn from_u128(value: u128) -> Self { - DiagnosticId(Uuid::from_u128(value)) + DiagnosticPath::new(buf) + } + + /// Returns full path, joined by `/` + pub fn as_str(&self) -> &str { + &self.path + } + + /// Returns an iterator over path components. + pub fn components(&self) -> impl Iterator + '_ { + self.path.split('/') } } -impl Default for DiagnosticId { - fn default() -> Self { - DiagnosticId(Uuid::new_v4()) +impl From for String { + fn from(path: DiagnosticPath) -> Self { + path.path.into() + } +} + +impl Eq for DiagnosticPath {} + +impl PartialEq for DiagnosticPath { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash && self.path == other.path + } +} + +impl Hash for DiagnosticPath { + fn hash(&self, state: &mut H) { + state.write_u64(self.hash); + } +} + +impl std::fmt::Display for DiagnosticPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.path.fmt(f) } } @@ -33,8 +117,7 @@ pub struct DiagnosticMeasurement { /// Diagnostic examples: frames per second, CPU usage, network latency #[derive(Debug)] pub struct Diagnostic { - pub id: DiagnosticId, - pub name: Cow<'static, str>, + path: DiagnosticPath, pub suffix: Cow<'static, str>, history: VecDeque, sum: f64, @@ -71,27 +154,13 @@ impl Diagnostic { self.history.push_back(measurement); } - /// Create a new diagnostic with the given ID, name and maximum history. - pub fn new( - id: DiagnosticId, - name: impl Into>, - max_history_length: usize, - ) -> Diagnostic { - let name = name.into(); - if name.chars().count() > MAX_DIAGNOSTIC_NAME_WIDTH { - // This could be a false positive due to a unicode width being shorter - warn!( - "Diagnostic {:?} has name longer than {} characters, and so might overflow in the LogDiagnosticsPlugin\ - Consider using a shorter name.", - name, MAX_DIAGNOSTIC_NAME_WIDTH - ); - } + /// Create a new diagnostic with the given path. + pub fn new(path: DiagnosticPath) -> Diagnostic { Diagnostic { - id, - name, + path, suffix: Cow::Borrowed(""), - history: VecDeque::with_capacity(max_history_length), - max_history_length, + history: VecDeque::with_capacity(DEFAULT_MAX_HISTORY_LENGTH), + max_history_length: DEFAULT_MAX_HISTORY_LENGTH, sum: 0.0, ema: 0.0, ema_smoothing_factor: 2.0 / 21.0, @@ -99,6 +168,15 @@ impl Diagnostic { } } + /// Set the maximum history length. + #[must_use] + pub fn with_max_history_length(mut self, max_history_length: usize) -> Self { + self.max_history_length = max_history_length; + self.history.reserve(self.max_history_length); + self.history.shrink_to(self.max_history_length); + self + } + /// Add a suffix to use when logging the value, can be used to show a unit. #[must_use] pub fn with_suffix(mut self, suffix: impl Into>) -> Self { @@ -122,6 +200,10 @@ impl Diagnostic { self } + pub fn path(&self) -> &DiagnosticPath { + &self.path + } + /// Get the latest measurement from this diagnostic. #[inline] pub fn measurement(&self) -> Option<&DiagnosticMeasurement> { @@ -198,9 +280,7 @@ impl Diagnostic { /// A collection of [`Diagnostic`]s. #[derive(Debug, Default, Resource)] pub struct DiagnosticsStore { - // This uses a [`StableHashMap`] to ensure that the iteration order is deterministic between - // runs when all diagnostics are inserted in the same order. - diagnostics: StableHashMap, + diagnostics: HashMap, } impl DiagnosticsStore { @@ -208,21 +288,21 @@ impl DiagnosticsStore { /// /// If possible, prefer calling [`App::register_diagnostic`]. pub fn add(&mut self, diagnostic: Diagnostic) { - self.diagnostics.insert(diagnostic.id, diagnostic); + self.diagnostics.insert(diagnostic.path.clone(), diagnostic); } - pub fn get(&self, id: DiagnosticId) -> Option<&Diagnostic> { - self.diagnostics.get(&id) + pub fn get(&self, path: &DiagnosticPath) -> Option<&Diagnostic> { + self.diagnostics.get(path) } - pub fn get_mut(&mut self, id: DiagnosticId) -> Option<&mut Diagnostic> { - self.diagnostics.get_mut(&id) + pub fn get_mut(&mut self, path: &DiagnosticPath) -> Option<&mut Diagnostic> { + self.diagnostics.get_mut(path) } /// Get the latest [`DiagnosticMeasurement`] from an enabled [`Diagnostic`]. - pub fn get_measurement(&self, id: DiagnosticId) -> Option<&DiagnosticMeasurement> { + pub fn get_measurement(&self, path: &DiagnosticPath) -> Option<&DiagnosticMeasurement> { self.diagnostics - .get(&id) + .get(path) .filter(|diagnostic| diagnostic.is_enabled) .and_then(|diagnostic| diagnostic.measurement()) } @@ -249,13 +329,13 @@ impl<'w, 's> Diagnostics<'w, 's> { /// Add a measurement to an enabled [`Diagnostic`]. The measurement is passed as a function so that /// it will be evaluated only if the [`Diagnostic`] is enabled. This can be useful if the value is /// costly to calculate. - pub fn add_measurement(&mut self, id: DiagnosticId, value: F) + pub fn add_measurement(&mut self, path: &DiagnosticPath, value: F) where F: FnOnce() -> f64, { if self .store - .get(id) + .get(path) .filter(|diagnostic| diagnostic.is_enabled) .is_some() { @@ -263,13 +343,13 @@ impl<'w, 's> Diagnostics<'w, 's> { time: Instant::now(), value: value(), }; - self.queue.0.insert(id, measurement); + self.queue.0.insert(path.clone(), measurement); } } } #[derive(Default)] -struct DiagnosticsBuffer(StableHashMap); +struct DiagnosticsBuffer(HashMap); impl SystemBuffer for DiagnosticsBuffer { fn apply( @@ -278,8 +358,8 @@ impl SystemBuffer for DiagnosticsBuffer { world: &mut bevy_ecs::world::World, ) { let mut diagnostics = world.resource_mut::(); - for (id, measurement) in self.0.drain() { - if let Some(diagnostic) = diagnostics.get_mut(id) { + for (path, measurement) in self.0.drain() { + if let Some(diagnostic) = diagnostics.get_mut(&path) { diagnostic.add_measurement(measurement); } } @@ -296,14 +376,14 @@ impl RegisterDiagnostic for App { /// /// Will initialize a [`DiagnosticsStore`] if it doesn't exist. /// - /// ```rust + /// ``` /// use bevy_app::App; - /// use bevy_diagnostic::{Diagnostic, DiagnosticsPlugin, DiagnosticId, RegisterDiagnostic}; + /// use bevy_diagnostic::{Diagnostic, DiagnosticsPlugin, DiagnosticPath, RegisterDiagnostic}; /// - /// const UNIQUE_DIAG_ID: DiagnosticId = DiagnosticId::from_u128(42); + /// const UNIQUE_DIAG_PATH: DiagnosticPath = DiagnosticPath::const_new("foo/bar"); /// /// App::new() - /// .register_diagnostic(Diagnostic::new(UNIQUE_DIAG_ID, "example", 10)) + /// .register_diagnostic(Diagnostic::new(UNIQUE_DIAG_PATH)) /// .add_plugins(DiagnosticsPlugin) /// .run(); /// ``` diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs index 6c501986b350a..91874a390c0f6 100644 --- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs @@ -1,7 +1,7 @@ use bevy_app::prelude::*; use bevy_ecs::entity::Entities; -use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic}; +use crate::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic}; /// Adds "entity count" diagnostic to an App. /// @@ -13,16 +13,15 @@ pub struct EntityCountDiagnosticsPlugin; impl Plugin for EntityCountDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT, "entity_count", 20)) + app.register_diagnostic(Diagnostic::new(Self::ENTITY_COUNT)) .add_systems(Update, Self::diagnostic_system); } } impl EntityCountDiagnosticsPlugin { - pub const ENTITY_COUNT: DiagnosticId = - DiagnosticId::from_u128(187513512115068938494459732780662867798); + pub const ENTITY_COUNT: DiagnosticPath = DiagnosticPath::const_new("entity_count"); pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) { - diagnostics.add_measurement(Self::ENTITY_COUNT, || entities.len() as f64); + 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 b5a8625821004..3586960443414 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -1,4 +1,4 @@ -use crate::{Diagnostic, DiagnosticId, Diagnostics, RegisterDiagnostic}; +use crate::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic}; use bevy_app::prelude::*; use bevy_core::FrameCount; use bevy_ecs::prelude::*; @@ -13,39 +13,33 @@ use bevy_time::{Real, Time}; pub struct FrameTimeDiagnosticsPlugin; impl Plugin for FrameTimeDiagnosticsPlugin { - fn build(&self, app: &mut App) { - app.register_diagnostic( - Diagnostic::new(Self::FRAME_TIME, "frame_time", 20).with_suffix("ms"), - ) - .register_diagnostic(Diagnostic::new(Self::FPS, "fps", 20)) - .register_diagnostic( - Diagnostic::new(Self::FRAME_COUNT, "frame_count", 1).with_smoothing_factor(0.0), - ) - .add_systems(Update, Self::diagnostic_system); + fn build(&self, app: &mut bevy_app::App) { + app.register_diagnostic(Diagnostic::new(Self::FRAME_TIME).with_suffix("ms")) + .register_diagnostic(Diagnostic::new(Self::FPS)) + .register_diagnostic(Diagnostic::new(Self::FRAME_COUNT).with_smoothing_factor(0.0)) + .add_systems(Update, Self::diagnostic_system); } } impl FrameTimeDiagnosticsPlugin { - pub const FPS: DiagnosticId = DiagnosticId::from_u128(288146834822086093791974408528866909483); - pub const FRAME_COUNT: DiagnosticId = - DiagnosticId::from_u128(54021991829115352065418785002088010277); - pub const FRAME_TIME: DiagnosticId = - DiagnosticId::from_u128(73441630925388532774622109383099159699); + pub const FPS: DiagnosticPath = DiagnosticPath::const_new("fps"); + pub const FRAME_COUNT: DiagnosticPath = DiagnosticPath::const_new("frame_count"); + pub const FRAME_TIME: DiagnosticPath = DiagnosticPath::const_new("frame_time"); pub fn diagnostic_system( mut diagnostics: Diagnostics, time: Res>, frame_count: Res, ) { - diagnostics.add_measurement(Self::FRAME_COUNT, || frame_count.0 as f64); + diagnostics.add_measurement(&Self::FRAME_COUNT, || frame_count.0 as f64); let delta_seconds = time.delta_seconds_f64(); if delta_seconds == 0.0 { return; } - diagnostics.add_measurement(Self::FRAME_TIME, || delta_seconds * 1000.0); + diagnostics.add_measurement(&Self::FRAME_TIME, || delta_seconds * 1000.0); - diagnostics.add_measurement(Self::FPS, || 1.0 / delta_seconds); + diagnostics.add_measurement(&Self::FPS, || 1.0 / delta_seconds); } } diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index aaa0ea449ab34..e16360a2d4962 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -1,3 +1,7 @@ +//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevyengine.org/). +//! It allows users to easily add diagnostic functionality to their Bevy applications, enhancing +//! their ability to monitor and optimize their game's. + mod diagnostic; mod entity_count_diagnostics_plugin; mod frame_time_diagnostics_plugin; @@ -24,6 +28,5 @@ impl Plugin for DiagnosticsPlugin { } } -/// The width which diagnostic names will be printed as -/// Plugin names should not be longer than this value -pub const MAX_DIAGNOSTIC_NAME_WIDTH: usize = 32; +/// Default max history length for new diagnostics. +pub const DEFAULT_MAX_HISTORY_LENGTH: usize = 120; diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index 7c24811869040..f3f57683086c1 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -1,4 +1,4 @@ -use super::{Diagnostic, DiagnosticId, DiagnosticsStore}; +use super::{Diagnostic, DiagnosticPath, DiagnosticsStore}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_log::{debug, info}; @@ -15,14 +15,14 @@ use bevy_utils::Duration; pub struct LogDiagnosticsPlugin { pub debug: bool, pub wait_duration: Duration, - pub filter: Option>, + pub filter: Option>, } /// State used by the [`LogDiagnosticsPlugin`] #[derive(Resource)] struct LogDiagnosticsState { timer: Timer, - filter: Option>, + filter: Option>, } impl Default for LogDiagnosticsPlugin { @@ -51,63 +51,84 @@ impl Plugin for LogDiagnosticsPlugin { } impl LogDiagnosticsPlugin { - pub fn filtered(filter: Vec) -> Self { + pub fn filtered(filter: Vec) -> Self { LogDiagnosticsPlugin { filter: Some(filter), ..Default::default() } } - fn log_diagnostic(diagnostic: &Diagnostic) { - if let Some(value) = diagnostic.smoothed() { - if diagnostic.get_max_history_length() > 1 { - if let Some(average) = diagnostic.average() { - info!( - target: "bevy diagnostic", - // Suffix is only used for 's' or 'ms' currently, - // so we reserve two columns for it; however, - // Do not reserve columns for the suffix in the average - // The ) hugging the value is more aesthetically pleasing - "{name:11.6}{suffix:2} (avg {average:>.6}{suffix:})", - name = diagnostic.name, - suffix = diagnostic.suffix, - name_width = crate::MAX_DIAGNOSTIC_NAME_WIDTH, - ); - return; + fn for_each_diagnostic( + state: &LogDiagnosticsState, + diagnostics: &DiagnosticsStore, + mut callback: impl FnMut(&Diagnostic), + ) { + if let Some(filter) = &state.filter { + for path in filter { + if let Some(diagnostic) = diagnostics.get(path) { + if diagnostic.is_enabled { + callback(diagnostic); + } + } + } + } else { + for diagnostic in diagnostics.iter() { + if diagnostic.is_enabled { + callback(diagnostic); } } + } + } + + fn log_diagnostic(path_width: usize, diagnostic: &Diagnostic) { + let Some(value) = diagnostic.smoothed() else { + return; + }; + + if diagnostic.get_max_history_length() > 1 { + let Some(average) = diagnostic.average() else { + return; + }; + info!( target: "bevy diagnostic", - "{name:.6}{suffix:}", - name = diagnostic.name, + // Suffix is only used for 's' or 'ms' currently, + // so we reserve two columns for it; however, + // Do not reserve columns for the suffix in the average + // The ) hugging the value is more aesthetically pleasing + "{path:11.6}{suffix:2} (avg {average:>.6}{suffix:})", + path = diagnostic.path(), + suffix = diagnostic.suffix, + ); + } else { + info!( + target: "bevy diagnostic", + "{path:.6}{suffix:}", + path = diagnostic.path(), suffix = diagnostic.suffix, - name_width = crate::MAX_DIAGNOSTIC_NAME_WIDTH, ); } } + fn log_diagnostics(state: &LogDiagnosticsState, diagnostics: &DiagnosticsStore) { + let mut path_width = 0; + Self::for_each_diagnostic(state, diagnostics, |diagnostic| { + let width = diagnostic.path().as_str().len(); + path_width = path_width.max(width); + }); + + Self::for_each_diagnostic(state, diagnostics, |diagnostic| { + Self::log_diagnostic(path_width, diagnostic); + }); + } + fn log_diagnostics_system( mut state: ResMut, time: Res>, diagnostics: Res, ) { if state.timer.tick(time.delta()).finished() { - if let Some(ref filter) = state.filter { - for diagnostic in filter.iter().flat_map(|id| { - diagnostics - .get(*id) - .filter(|diagnostic| diagnostic.is_enabled) - }) { - Self::log_diagnostic(diagnostic); - } - } else { - for diagnostic in diagnostics - .iter() - .filter(|diagnostic| diagnostic.is_enabled) - { - Self::log_diagnostic(diagnostic); - } - } + Self::log_diagnostics(&state, &diagnostics); } } @@ -117,22 +138,9 @@ impl LogDiagnosticsPlugin { diagnostics: Res, ) { if state.timer.tick(time.delta()).finished() { - if let Some(ref filter) = state.filter { - for diagnostic in filter.iter().flat_map(|id| { - diagnostics - .get(*id) - .filter(|diagnostic| diagnostic.is_enabled) - }) { - debug!("{:#?}\n", diagnostic); - } - } else { - for diagnostic in diagnostics - .iter() - .filter(|diagnostic| diagnostic.is_enabled) - { - debug!("{:#?}\n", diagnostic); - } - } + 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 46c39a27977ff..e8c88a2f5a91e 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -1,4 +1,4 @@ -use crate::DiagnosticId; +use crate::DiagnosticPath; use bevy_app::prelude::*; /// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %) @@ -24,10 +24,8 @@ impl Plugin for SystemInformationDiagnosticsPlugin { } impl SystemInformationDiagnosticsPlugin { - pub const CPU_USAGE: DiagnosticId = - DiagnosticId::from_u128(78494871623549551581510633532637320956); - pub const MEM_USAGE: DiagnosticId = - DiagnosticId::from_u128(42846254859293759601295317811892519825); + pub const CPU_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/cpu_usage"); + pub const MEM_USAGE: DiagnosticPath = DiagnosticPath::const_new("system/mem_usage"); } // NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on wasm @@ -43,29 +41,19 @@ impl SystemInformationDiagnosticsPlugin { pub mod internal { use bevy_ecs::{prelude::ResMut, system::Local}; use bevy_log::info; - use sysinfo::{CpuExt, CpuRefreshKind, RefreshKind, System, SystemExt}; + use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; use crate::{Diagnostic, Diagnostics, DiagnosticsStore}; + use super::SystemInformationDiagnosticsPlugin; + const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0; pub(crate) fn setup_system(mut diagnostics: ResMut) { - diagnostics.add( - Diagnostic::new( - super::SystemInformationDiagnosticsPlugin::CPU_USAGE, - "cpu_usage", - 20, - ) - .with_suffix("%"), - ); - diagnostics.add( - Diagnostic::new( - super::SystemInformationDiagnosticsPlugin::MEM_USAGE, - "mem_usage", - 20, - ) - .with_suffix("%"), - ); + diagnostics + .add(Diagnostic::new(SystemInformationDiagnosticsPlugin::CPU_USAGE).with_suffix("%")); + diagnostics + .add(Diagnostic::new(SystemInformationDiagnosticsPlugin::MEM_USAGE).with_suffix("%")); } pub(crate) fn diagnostic_system( @@ -76,7 +64,7 @@ pub mod internal { *sysinfo = Some(System::new_with_specifics( RefreshKind::new() .with_cpu(CpuRefreshKind::new().with_cpu_usage()) - .with_memory(), + .with_memory(MemoryRefreshKind::everything()), )); } let Some(sys) = sysinfo.as_mut() else { @@ -91,10 +79,10 @@ pub mod internal { let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB; let current_used_mem = used_mem / total_mem * 100.0; - diagnostics.add_measurement(super::SystemInformationDiagnosticsPlugin::CPU_USAGE, || { + diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || { current_cpu_usage as f64 }); - diagnostics.add_measurement(super::SystemInformationDiagnosticsPlugin::MEM_USAGE, || { + diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || { current_used_mem }); } @@ -116,12 +104,8 @@ pub mod internal { sys.refresh_memory(); let info = SystemInfo { - os: sys - .long_os_version() - .unwrap_or_else(|| String::from("not available")), - kernel: sys - .kernel_version() - .unwrap_or_else(|| String::from("not available")), + os: System::long_os_version().unwrap_or_else(|| String::from("not available")), + kernel: System::kernel_version().unwrap_or_else(|| String::from("not available")), cpu: sys.global_cpu_info().brand().trim().to_string(), core_count: sys .physical_core_count() diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 3231e7b4101ff..da5bc549b9cbc 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -21,8 +21,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.12.0" } bevy_utils = { path = "../bevy_utils", version = "0.12.0" } bevy_ecs_macros = { path = "macros", version = "0.12.0" } -async-channel = "1.4" -event-listener = "2.5" +async-channel = "2.1.0" thread_local = "1.1.4" fixedbitset = "0.4.2" rustc-hash = "1.1" diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 5d96ee2b3a687..f9d3f0314ba02 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -208,6 +208,8 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { type State = (#(#param::State,)*); type Item<'w, 's> = ParamSet<'w, 's, (#(#param,)*)>; + // Note: We allow non snake case so the compiler don't complain about the creation of non_snake_case variables + #[allow(non_snake_case)] 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 @@ -215,6 +217,7 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { #meta.component_access_set.clear(); #meta.archetype_component_access.clear(); #param::init_state(world, &mut #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. diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 5196d25deb192..0206b10d8c669 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -37,7 +37,6 @@ pub(crate) fn item_struct( Fields::Unnamed(_) => quote! { #derive_macro_call #item_attrs - #[automatically_derived] #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world( #( #field_visibilities <#field_types as #path::query::WorldQuery>::Item<'__w>, )* ); @@ -165,22 +164,18 @@ pub(crate) fn world_query_impl( #( <#field_types>::update_component_access(&state.#named_field_idents, _access); )* } - fn update_archetype_component_access( - state: &Self::State, - _archetype: &#path::archetype::Archetype, - _access: &mut #path::query::Access<#path::archetype::ArchetypeComponentId> - ) { - #( - <#field_types>::update_archetype_component_access(&state.#named_field_idents, _archetype, _access); - )* - } - fn init_state(world: &mut #path::world::World) -> #state_struct_name #user_ty_generics { #state_struct_name { #(#named_field_idents: <#field_types>::init_state(world),)* } } + fn get_state(world: &#path::world::World) -> Option<#state_struct_name #user_ty_generics> { + Some(#state_struct_name { + #(#named_field_idents: <#field_types>::get_state(world)?,)* + }) + } + fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(#path::component::ComponentId) -> bool) -> bool { true #(&& <#field_types>::matches_component_set(&state.#named_field_idents, _set_contains_id))* } diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 3e23da6457739..9201f154aca99 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -272,7 +272,7 @@ pub struct ArchetypeEntity { impl ArchetypeEntity { /// The ID of the entity. #[inline] - pub const fn entity(&self) -> Entity { + pub const fn id(&self) -> Entity { self.entity } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 99a4fe747dbbe..08710679a5325 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -146,7 +146,7 @@ pub trait DetectChangesMut: DetectChanges { /// } /// # let mut world = World::new(); /// # world.insert_resource(Score(1)); - /// # let mut score_changed = IntoSystem::into_system(resource_changed::()); + /// # let mut score_changed = IntoSystem::into_system(resource_changed::); /// # score_changed.initialize(&mut world); /// # score_changed.run((), &mut world); /// # @@ -210,7 +210,7 @@ pub trait DetectChangesMut: DetectChanges { /// # let mut world = World::new(); /// # world.insert_resource(Events::::default()); /// # world.insert_resource(Score(1)); - /// # let mut score_changed = IntoSystem::into_system(resource_changed::()); + /// # let mut score_changed = IntoSystem::into_system(resource_changed::); /// # score_changed.initialize(&mut world); /// # score_changed.run((), &mut world); /// # @@ -358,7 +358,7 @@ macro_rules! impl_methods { /// You should never modify the argument passed to the closure -- if you want to modify the data /// without flagging a change, consider using [`DetectChangesMut::bypass_change_detection`] to make your intent explicit. /// - /// ```rust + /// ``` /// # use bevy_ecs::prelude::*; /// # #[derive(PartialEq)] pub struct Vec2; /// # impl Vec2 { pub const ZERO: Self = Self; } diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 3e0d28df7d3a7..e017ea4f5498a 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -553,7 +553,7 @@ impl Components { /// Returns [`None`] if the `Component` type has not /// yet been initialized using [`Components::init_component()`]. /// - /// ```rust + /// ``` /// use bevy_ecs::prelude::*; /// /// let mut world = World::new(); @@ -591,7 +591,7 @@ impl Components { /// Returns [`None`] if the `Resource` type has not /// yet been initialized using [`Components::init_resource()`]. /// - /// ```rust + /// ``` /// use bevy_ecs::prelude::*; /// /// let mut world = World::new(); @@ -807,7 +807,7 @@ impl ComponentTicks { /// However, components and resources that make use of interior mutability might require manual updates. /// /// # Example - /// ```rust,no_run + /// ```no_run /// # use bevy_ecs::{world::World, component::ComponentTicks}; /// let world: World = unimplemented!(); /// let component_ticks: ComponentTicks = unimplemented!(); @@ -823,7 +823,7 @@ impl ComponentTicks { /// A [`SystemParam`] that provides access to the [`ComponentId`] for a specific type. /// /// # Example -/// ```rust +/// ``` /// # use bevy_ecs::{system::Local, component::{Component, ComponentId, ComponentIdFor}}; /// #[derive(Component)] /// struct Player; diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index 19890205723b8..4c99ff69d5f51 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -1,4 +1,8 @@ -use crate::{entity::Entity, world::World}; +use crate::{ + entity::Entity, + identifier::masks::{IdentifierMask, HIGH_MASK}, + world::World, +}; use bevy_utils::EntityHashMap; /// Operation to map all contained [`Entity`] fields in a type to new values. @@ -12,7 +16,7 @@ use bevy_utils::EntityHashMap; /// /// ## Example /// -/// ```rust +/// ``` /// use bevy_ecs::prelude::*; /// use bevy_ecs::entity::{EntityMapper, MapEntities}; /// @@ -68,11 +72,13 @@ impl<'m> EntityMapper<'m> { } // this new entity reference is specifically designed to never represent any living entity - let new = Entity { - generation: self.dead_start.generation + self.generations, - index: self.dead_start.index, - }; - self.generations += 1; + let new = Entity::from_raw_and_generation( + self.dead_start.index(), + IdentifierMask::inc_masked_high_by(self.dead_start.generation, self.generations), + ); + + // Prevent generations counter from being a greater value than HIGH_MASK. + self.generations = (self.generations + 1) & HIGH_MASK; self.map.insert(entity, new); @@ -107,7 +113,7 @@ impl<'m> EntityMapper<'m> { // SAFETY: Entities data is kept in a valid state via `EntityMap::world_scope` let entities = unsafe { world.entities_mut() }; assert!(entities.free(self.dead_start).is_some()); - assert!(entities.reserve_generations(self.dead_start.index, self.generations)); + assert!(entities.reserve_generations(self.dead_start.index(), self.generations)); } /// Creates an [`EntityMapper`] from a provided [`World`] and [`EntityHashMap`], then calls the @@ -146,7 +152,7 @@ mod tests { let mut world = World::new(); let mut mapper = EntityMapper::new(&mut map, &mut world); - let mapped_ent = Entity::new(FIRST_IDX, 0); + let mapped_ent = Entity::from_raw(FIRST_IDX); let dead_ref = mapper.get_or_reserve(mapped_ent); assert_eq!( @@ -155,7 +161,7 @@ mod tests { "should persist the allocated mapping from the previous line" ); assert_eq!( - mapper.get_or_reserve(Entity::new(SECOND_IDX, 0)).index(), + mapper.get_or_reserve(Entity::from_raw(SECOND_IDX)).index(), dead_ref.index(), "should re-use the same index for further dead refs" ); @@ -173,7 +179,7 @@ mod tests { let mut world = World::new(); let dead_ref = EntityMapper::world_scope(&mut map, &mut world, |_, mapper| { - mapper.get_or_reserve(Entity::new(0, 0)) + mapper.get_or_reserve(Entity::from_raw(0)) }); // Next allocated entity should be a further generation on the same index diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 61c12e2fa7f5d..ab7d167cff67c 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -37,14 +37,21 @@ //! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove mod map_entities; +use bevy_utils::tracing::warn; pub use map_entities::*; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, + identifier::{ + error::IdentifierError, + kinds::IdKind, + masks::{IdentifierMask, HIGH_MASK}, + Identifier, + }, storage::{SparseSetIndex, TableId, TableRow}, }; use serde::{Deserialize, Serialize}; -use std::{convert::TryFrom, fmt, hash::Hash, mem, sync::atomic::Ordering}; +use std::{convert::TryFrom, fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering}; #[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicI64 as AtomicIdCursor; @@ -124,7 +131,7 @@ pub struct Entity { // to make this struct equivalent to a u64. #[cfg(target_endian = "little")] index: u32, - generation: u32, + generation: NonZeroU32, #[cfg(target_endian = "big")] index: u32, } @@ -187,9 +194,13 @@ pub(crate) enum AllocAtWithoutReplacement { } impl Entity { - #[cfg(test)] - pub(crate) const fn new(index: u32, generation: u32) -> Entity { - Entity { index, generation } + /// Construct an [`Entity`] from a raw `index` value and a non-zero `generation` value. + /// Ensure that the generation value is never greater than `0x7FFF_FFFF`. + #[inline(always)] + pub(crate) const fn from_raw_and_generation(index: u32, generation: NonZeroU32) -> Entity { + debug_assert!(generation.get() <= HIGH_MASK); + + Self { index, generation } } /// An entity ID with a placeholder value. This may or may not correspond to an actual entity, @@ -228,7 +239,7 @@ impl Entity { /// ``` pub const PLACEHOLDER: Self = Self::from_raw(u32::MAX); - /// Creates a new entity ID with the specified `index` and a generation of 0. + /// Creates a new entity ID with the specified `index` and a generation of 1. /// /// # Note /// @@ -240,12 +251,9 @@ impl Entity { /// In general, one should not try to synchronize the ECS by attempting to ensure that /// `Entity` lines up between instances, but instead insert a secondary identifier as /// a component. - #[inline] + #[inline(always)] pub const fn from_raw(index: u32) -> Entity { - Entity { - index, - generation: 0, - } + Self::from_raw_and_generation(index, NonZeroU32::MIN) } /// Convert to a form convenient for passing outside of rust. @@ -256,20 +264,48 @@ impl Entity { /// No particular structure is guaranteed for the returned bits. #[inline(always)] pub const fn to_bits(self) -> u64 { - (self.generation as u64) << 32 | self.index as u64 + IdentifierMask::pack_into_u64(self.index, self.generation.get()) } /// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`]. /// /// Only useful when applied to results from `to_bits` in the same instance of an application. - #[inline(always)] + /// + /// # Panics + /// + /// This method will likely panic if given `u64` values that did not come from [`Entity::to_bits`]. + #[inline] pub const fn from_bits(bits: u64) -> Self { - Self { - generation: (bits >> 32) as u32, - index: bits as u32, + // Construct an Identifier initially to extract the kind from. + let id = Self::try_from_bits(bits); + + match id { + Ok(entity) => entity, + Err(_) => panic!("Attempted to initialise invalid bits as an entity"), } } + /// Reconstruct an `Entity` previously destructured with [`Entity::to_bits`]. + /// + /// Only useful when applied to results from `to_bits` in the same instance of an application. + /// + /// This method is the fallible counterpart to [`Entity::from_bits`]. + #[inline(always)] + pub const fn try_from_bits(bits: u64) -> Result { + if let Ok(id) = Identifier::try_from_bits(bits) { + let kind = id.kind() as u8; + + if kind == (IdKind::Entity as u8) { + return Ok(Self { + index: id.low(), + generation: id.high(), + }); + } + } + + Err(IdentifierError::InvalidEntityId(bits)) + } + /// Return a transiently unique identifier. /// /// No two simultaneously-live entities share the same index, but dead entities' indices may collide @@ -285,7 +321,24 @@ impl Entity { /// given index has been reused (index, generation) pairs uniquely identify a given Entity. #[inline] pub const fn generation(self) -> u32 { - self.generation + // Mask so not to expose any flags + IdentifierMask::extract_value_from_high(self.generation.get()) + } +} + +impl TryFrom for Entity { + type Error = IdentifierError; + + #[inline] + fn try_from(value: Identifier) -> Result { + Self::try_from_bits(value.to_bits()) + } +} + +impl From for Identifier { + #[inline] + fn from(value: Entity) -> Self { + Identifier::from_bits(value.to_bits()) } } @@ -303,14 +356,15 @@ impl<'de> Deserialize<'de> for Entity { where D: serde::Deserializer<'de>, { + use serde::de::Error; let id: u64 = serde::de::Deserialize::deserialize(deserializer)?; - Ok(Entity::from_bits(id)) + Entity::try_from_bits(id).map_err(D::Error::custom) } } impl fmt::Debug for Entity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}v{}", self.index, self.generation) + write!(f, "{}v{}", self.index(), self.generation()) } } @@ -344,16 +398,10 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { fn next(&mut self) -> Option { self.index_iter .next() - .map(|&index| Entity { - generation: self.meta[index as usize].generation, - index, - }) - .or_else(|| { - self.index_range.next().map(|index| Entity { - generation: 0, - index, - }) + .map(|&index| { + Entity::from_raw_and_generation(index, self.meta[index as usize].generation) }) + .or_else(|| self.index_range.next().map(Entity::from_raw)) } fn size_hint(&self) -> (usize, Option) { @@ -436,6 +484,7 @@ impl Entities { /// Reserve entity IDs concurrently. /// /// Storage for entity generation and location is lazily allocated by calling [`flush`](Entities::flush). + #[allow(clippy::unnecessary_fallible_conversions)] // Because `IdCursor::try_from` may fail on 32-bit platforms. pub fn reserve_entities(&self, count: u32) -> ReserveEntitiesIterator { // Use one atomic subtract to grab a range of new IDs. The range might be // entirely nonnegative, meaning all IDs come from the freelist, or entirely @@ -487,20 +536,16 @@ impl Entities { if n > 0 { // Allocate from the freelist. let index = self.pending[(n - 1) as usize]; - Entity { - generation: self.meta[index as usize].generation, - index, - } + Entity::from_raw_and_generation(index, self.meta[index as usize].generation) } else { // Grab a new ID, outside the range of `meta.len()`. `flush()` must // eventually be called to make it valid. // // As `self.free_cursor` goes more and more negative, we return IDs farther // and farther beyond `meta.len()`. - Entity { - generation: 0, - index: u32::try_from(self.meta.len() as IdCursor - n).expect("too many entities"), - } + Entity::from_raw( + u32::try_from(self.meta.len() as IdCursor - n).expect("too many entities"), + ) } } @@ -519,17 +564,11 @@ impl Entities { if let Some(index) = self.pending.pop() { let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - Entity { - generation: self.meta[index as usize].generation, - index, - } + Entity::from_raw_and_generation(index, self.meta[index as usize].generation) } else { let index = u32::try_from(self.meta.len()).expect("too many entities"); self.meta.push(EntityMeta::EMPTY); - Entity { - generation: 0, - index, - } + Entity::from_raw(index) } } @@ -540,15 +579,16 @@ impl Entities { pub fn alloc_at(&mut self, entity: Entity) -> Option { self.verify_flushed(); - let loc = if entity.index as usize >= self.meta.len() { - self.pending.extend((self.meta.len() as u32)..entity.index); + let loc = if entity.index() as usize >= self.meta.len() { + self.pending + .extend((self.meta.len() as u32)..entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; self.meta - .resize(entity.index as usize + 1, EntityMeta::EMPTY); + .resize(entity.index() as usize + 1, EntityMeta::EMPTY); self.len += 1; None - } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index) { + } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { self.pending.swap_remove(index); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; @@ -556,12 +596,12 @@ impl Entities { None } else { Some(mem::replace( - &mut self.meta[entity.index as usize].location, + &mut self.meta[entity.index() as usize].location, EntityMeta::EMPTY.location, )) }; - self.meta[entity.index as usize].generation = entity.generation; + self.meta[entity.index() as usize].generation = entity.generation; loc } @@ -575,22 +615,23 @@ impl Entities { ) -> AllocAtWithoutReplacement { self.verify_flushed(); - let result = if entity.index as usize >= self.meta.len() { - self.pending.extend((self.meta.len() as u32)..entity.index); + let result = if entity.index() as usize >= self.meta.len() { + self.pending + .extend((self.meta.len() as u32)..entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; self.meta - .resize(entity.index as usize + 1, EntityMeta::EMPTY); + .resize(entity.index() as usize + 1, EntityMeta::EMPTY); self.len += 1; AllocAtWithoutReplacement::DidNotExist - } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index) { + } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { self.pending.swap_remove(index); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; self.len += 1; AllocAtWithoutReplacement::DidNotExist } else { - let current_meta = &self.meta[entity.index as usize]; + let current_meta = &self.meta[entity.index() as usize]; if current_meta.location.archetype_id == ArchetypeId::INVALID { AllocAtWithoutReplacement::DidNotExist } else if current_meta.generation == entity.generation { @@ -600,7 +641,7 @@ impl Entities { } }; - self.meta[entity.index as usize].generation = entity.generation; + self.meta[entity.index() as usize].generation = entity.generation; result } @@ -610,15 +651,23 @@ impl Entities { pub fn free(&mut self, entity: Entity) -> Option { self.verify_flushed(); - let meta = &mut self.meta[entity.index as usize]; + let meta = &mut self.meta[entity.index() as usize]; if meta.generation != entity.generation { return None; } - meta.generation += 1; + + meta.generation = IdentifierMask::inc_masked_high_by(meta.generation, 1); + + if meta.generation == NonZeroU32::MIN { + warn!( + "Entity({}) generation wrapped on Entities::free, aliasing may occur", + entity.index + ); + } let loc = mem::replace(&mut meta.location, EntityMeta::EMPTY.location); - self.pending.push(entity.index); + self.pending.push(entity.index()); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; @@ -627,6 +676,7 @@ impl Entities { } /// Ensure at least `n` allocations can succeed without reallocating. + #[allow(clippy::unnecessary_fallible_conversions)] // Because `IdCursor::try_from` may fail on 32-bit platforms. pub fn reserve(&mut self, additional: u32) { self.verify_flushed(); @@ -644,7 +694,7 @@ impl Entities { // not reallocated since the generation is incremented in `free` pub fn contains(&self, entity: Entity) -> bool { self.resolve_from_id(entity.index()) - .map_or(false, |e| e.generation() == entity.generation) + .map_or(false, |e| e.generation() == entity.generation()) } /// Clears all [`Entity`] from the World. @@ -659,7 +709,7 @@ impl Entities { /// Note: for pending entities, returns `Some(EntityLocation::INVALID)`. #[inline] pub fn get(&self, entity: Entity) -> Option { - if let Some(meta) = self.meta.get(entity.index as usize) { + if let Some(meta) = self.meta.get(entity.index() as usize) { if meta.generation != entity.generation || meta.location.archetype_id == ArchetypeId::INVALID { @@ -696,7 +746,7 @@ impl Entities { let meta = &mut self.meta[index as usize]; if meta.location.archetype_id == ArchetypeId::INVALID { - meta.generation += generations; + meta.generation = IdentifierMask::inc_masked_high_by(meta.generation, generations); true } else { false @@ -712,17 +762,14 @@ impl Entities { pub fn resolve_from_id(&self, index: u32) -> Option { let idu = index as usize; if let Some(&EntityMeta { generation, .. }) = self.meta.get(idu) { - Some(Entity { generation, index }) + Some(Entity::from_raw_and_generation(index, generation)) } else { // `id` is outside of the meta list - check whether it is reserved but not yet flushed. let free_cursor = self.free_cursor.load(Ordering::Relaxed); // If this entity was manually created, then free_cursor might be positive // Returning None handles that case correctly let num_pending = usize::try_from(-free_cursor).ok()?; - (idu < self.meta.len() + num_pending).then_some(Entity { - generation: 0, - index, - }) + (idu < self.meta.len() + num_pending).then_some(Entity::from_raw(index)) } } @@ -753,10 +800,7 @@ impl Entities { self.len += -current_free_cursor as u32; for (index, meta) in self.meta.iter_mut().enumerate().skip(old_meta_len) { init( - Entity { - index: index as u32, - generation: meta.generation, - }, + Entity::from_raw_and_generation(index as u32, meta.generation), &mut meta.location, ); } @@ -769,10 +813,7 @@ impl Entities { for index in self.pending.drain(new_free_cursor..) { let meta = &mut self.meta[index as usize]; init( - Entity { - index, - generation: meta.generation, - }, + Entity::from_raw_and_generation(index, meta.generation), &mut meta.location, ); } @@ -838,7 +879,7 @@ impl Entities { #[repr(C)] struct EntityMeta { /// The current generation of the [`Entity`]. - pub generation: u32, + pub generation: NonZeroU32, /// The current location of the [`Entity`] pub location: EntityLocation, } @@ -846,7 +887,7 @@ struct EntityMeta { impl EntityMeta { /// meta for **pending entity** const EMPTY: EntityMeta = EntityMeta { - generation: 0, + generation: NonZeroU32::MIN, location: EntityLocation::INVALID, }; } @@ -894,12 +935,18 @@ impl EntityLocation { mod tests { use super::*; + #[test] + fn entity_niche_optimization() { + assert_eq!( + std::mem::size_of::(), + std::mem::size_of::>() + ); + } + #[test] fn entity_bits_roundtrip() { - let e = Entity { - generation: 0xDEADBEEF, - index: 0xBAADF00D, - }; + // Generation cannot be greater than 0x7FFF_FFFF else it will be an invalid Entity id + let e = Entity::from_raw_and_generation(0xDEADBEEF, NonZeroU32::new(0x5AADF00D).unwrap()); assert_eq!(Entity::from_bits(e.to_bits()), e); } @@ -933,12 +980,12 @@ mod tests { #[test] fn entity_const() { const C1: Entity = Entity::from_raw(42); - assert_eq!(42, C1.index); - assert_eq!(0, C1.generation); + assert_eq!(42, C1.index()); + assert_eq!(1, C1.generation()); const C2: Entity = Entity::from_bits(0x0000_00ff_0000_00cc); - assert_eq!(0x0000_00cc, C2.index); - assert_eq!(0x0000_00ff, C2.generation); + assert_eq!(0x0000_00cc, C2.index()); + assert_eq!(0x0000_00ff, C2.generation()); const C3: u32 = Entity::from_raw(33).index(); assert_eq!(33, C3); @@ -953,7 +1000,7 @@ mod tests { let entity = entities.alloc(); entities.free(entity); - assert!(entities.reserve_generations(entity.index, 1)); + assert!(entities.reserve_generations(entity.index(), 1)); } #[test] @@ -964,12 +1011,12 @@ mod tests { let entity = entities.alloc(); entities.free(entity); - assert!(entities.reserve_generations(entity.index, GENERATIONS)); + assert!(entities.reserve_generations(entity.index(), GENERATIONS)); // 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 + GENERATIONS); + assert!(next_entity.generation() > entity.generation() + GENERATIONS); } #[test] @@ -977,25 +1024,67 @@ mod tests { // This is intentionally testing `lt` and `ge` as separate functions. #![allow(clippy::nonminimal_bool)] - assert_eq!(Entity::new(123, 456), Entity::new(123, 456)); - assert_ne!(Entity::new(123, 789), Entity::new(123, 456)); - assert_ne!(Entity::new(123, 456), Entity::new(123, 789)); - assert_ne!(Entity::new(123, 456), Entity::new(456, 123)); + assert_eq!( + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + ); + assert_ne!( + Entity::from_raw_and_generation(123, NonZeroU32::new(789).unwrap()), + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + ); + assert_ne!( + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), + Entity::from_raw_and_generation(123, NonZeroU32::new(789).unwrap()) + ); + assert_ne!( + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()), + Entity::from_raw_and_generation(456, NonZeroU32::new(123).unwrap()) + ); // ordering is by generation then by index - assert!(Entity::new(123, 456) >= Entity::new(123, 456)); - assert!(Entity::new(123, 456) <= Entity::new(123, 456)); - assert!(!(Entity::new(123, 456) < Entity::new(123, 456))); - assert!(!(Entity::new(123, 456) > Entity::new(123, 456))); + assert!( + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + >= Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + ); + assert!( + Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + <= Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + ); + assert!( + !(Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + < Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap())) + ); + assert!( + !(Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap()) + > Entity::from_raw_and_generation(123, NonZeroU32::new(456).unwrap())) + ); - assert!(Entity::new(9, 1) < Entity::new(1, 9)); - assert!(Entity::new(1, 9) > Entity::new(9, 1)); + assert!( + Entity::from_raw_and_generation(9, NonZeroU32::new(1).unwrap()) + < Entity::from_raw_and_generation(1, NonZeroU32::new(9).unwrap()) + ); + assert!( + Entity::from_raw_and_generation(1, NonZeroU32::new(9).unwrap()) + > Entity::from_raw_and_generation(9, NonZeroU32::new(1).unwrap()) + ); - assert!(Entity::new(1, 1) < Entity::new(2, 1)); - assert!(Entity::new(1, 1) <= Entity::new(2, 1)); - assert!(Entity::new(2, 2) > Entity::new(1, 2)); - assert!(Entity::new(2, 2) >= Entity::new(1, 2)); + assert!( + Entity::from_raw_and_generation(1, NonZeroU32::new(1).unwrap()) + < Entity::from_raw_and_generation(2, NonZeroU32::new(1).unwrap()) + ); + assert!( + Entity::from_raw_and_generation(1, NonZeroU32::new(1).unwrap()) + <= Entity::from_raw_and_generation(2, NonZeroU32::new(1).unwrap()) + ); + assert!( + Entity::from_raw_and_generation(2, NonZeroU32::new(2).unwrap()) + > Entity::from_raw_and_generation(1, NonZeroU32::new(2).unwrap()) + ); + assert!( + Entity::from_raw_and_generation(2, NonZeroU32::new(2).unwrap()) + >= Entity::from_raw_and_generation(1, NonZeroU32::new(2).unwrap()) + ); } // Feel free to change this test if needed, but it seemed like an important diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 7e2284dd4850a..8b25ab2a2cbc8 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -407,6 +407,11 @@ impl DerefMut for EventSequence { } /// Reads events of type `T` in order and tracks which events have already been read. +/// +/// # Concurrency +/// +/// Unlike [`EventWriter`], systems with `EventReader` param can be executed concurrently +/// (but not concurrently with `EventWriter` systems for the same event type). #[derive(SystemParam, Debug)] pub struct EventReader<'w, 's, E: Event> { reader: Local<'s, ManualEventReader>, @@ -484,7 +489,12 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// # bevy_ecs::system::assert_is_system(my_system); /// ``` /// -/// # Limitations +/// # Concurrency +/// +/// `EventWriter` param has [`ResMut>`](Events) inside. So two systems declaring `EventWriter` params +/// for the same event type won't be executed concurrently. +/// +/// # Untyped events /// /// `EventWriter` can only send events of one specific type, which must be known at compile-time. /// This is not a problem most of the time, but you may find a situation where you cannot know diff --git a/crates/bevy_ecs/src/identifier/error.rs b/crates/bevy_ecs/src/identifier/error.rs new file mode 100644 index 0000000000000..8974ce954b2d9 --- /dev/null +++ b/crates/bevy_ecs/src/identifier/error.rs @@ -0,0 +1,29 @@ +//! Error types for [`super::Identifier`] conversions. An ID can be converted +//! to various kinds, but these can fail if they are not valid forms of those +//! kinds. The error type in this module encapsulates the various failure modes. +use std::fmt; + +/// An Error type for [`super::Identifier`], mostly for providing error +/// handling for convertions of an ID to a type abstracting over the ID bits. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum IdentifierError { + /// A given ID has an invalid value for initialising to a [`crate::identifier::Identifier`]. + InvalidIdentifier, + /// A given ID has an invalid configuration of bits for converting to an [`crate::entity::Entity`]. + InvalidEntityId(u64), +} + +impl fmt::Display for IdentifierError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidIdentifier => write!( + f, + "The given id contains a zero value high component, which is invalid" + ), + Self::InvalidEntityId(_) => write!(f, "The given id is not a valid entity."), + } + } +} + +impl std::error::Error for IdentifierError {} diff --git a/crates/bevy_ecs/src/identifier/kinds.rs b/crates/bevy_ecs/src/identifier/kinds.rs new file mode 100644 index 0000000000000..a5f57365fc1f0 --- /dev/null +++ b/crates/bevy_ecs/src/identifier/kinds.rs @@ -0,0 +1,11 @@ +/// The kinds of ID that [`super::Identifier`] can represent. Each +/// variant imposes different usages of the low/high segments +/// of the ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum IdKind { + /// An ID variant that is compatible with [`crate::entity::Entity`]. + Entity = 0, + /// A future ID variant. + Placeholder = 0b1000_0000, +} diff --git a/crates/bevy_ecs/src/identifier/masks.rs b/crates/bevy_ecs/src/identifier/masks.rs new file mode 100644 index 0000000000000..7751ce16be3f8 --- /dev/null +++ b/crates/bevy_ecs/src/identifier/masks.rs @@ -0,0 +1,233 @@ +use std::num::NonZeroU32; + +use super::kinds::IdKind; + +/// Mask for extracting the value portion of a 32-bit high segment. This +/// yields 31-bits of total value, as the final bit (the most significant) +/// is reserved as a flag bit. Can be negated to extract the flag bit. +pub(crate) const HIGH_MASK: u32 = 0x7FFF_FFFF; + +/// Abstraction over masks needed to extract values/components of an [`super::Identifier`]. +pub(crate) struct IdentifierMask; + +impl IdentifierMask { + /// Returns the low component from a `u64` value + #[inline(always)] + pub(crate) const fn get_low(value: u64) -> u32 { + // This will truncate to the lowest 32 bits + value as u32 + } + + /// Returns the high component from a `u64` value + #[inline(always)] + pub(crate) const fn get_high(value: u64) -> u32 { + // This will discard the lowest 32 bits + (value >> u32::BITS) as u32 + } + + /// Pack a low and high `u32` values into a single `u64` value. + #[inline(always)] + pub(crate) const fn pack_into_u64(low: u32, high: u32) -> u64 { + ((high as u64) << u32::BITS) | (low as u64) + } + + /// Pack the [`IdKind`] bits into a high segment. + #[inline(always)] + pub(crate) const fn pack_kind_into_high(value: u32, kind: IdKind) -> u32 { + value | ((kind as u32) << 24) + } + + /// Extract the value component from a high segment of an [`super::Identifier`]. + #[inline(always)] + pub(crate) const fn extract_value_from_high(value: u32) -> u32 { + value & HIGH_MASK + } + + /// Extract the ID kind component from a high segment of an [`super::Identifier`]. + #[inline(always)] + pub(crate) const fn extract_kind_from_high(value: u32) -> IdKind { + // The negated HIGH_MASK will extract just the bit we need for kind. + let kind_mask = !HIGH_MASK; + let bit = value & kind_mask; + + if bit == kind_mask { + IdKind::Placeholder + } else { + IdKind::Entity + } + } + + /// Offsets a masked generation value by the specified amount, wrapping to 1 instead of 0. + /// Will never be greater than [`HIGH_MASK`] or less than `1`, and increments are masked to + /// never be greater than [`HIGH_MASK`]. + #[inline(always)] + pub(crate) const fn inc_masked_high_by(lhs: NonZeroU32, rhs: u32) -> NonZeroU32 { + let lo = (lhs.get() & HIGH_MASK).wrapping_add(rhs & HIGH_MASK); + // Checks high 32 bit for whether we have overflowed 31 bits. + let overflowed = lo >> 31; + + // SAFETY: + // - Adding the overflow flag will offet overflows to start at 1 instead of 0 + // - The sum of `0x7FFF_FFFF` + `u32::MAX` + 1 (overflow) == `0x7FFF_FFFF` + // - If the operation doesn't overflow at 31 bits, no offsetting takes place + unsafe { NonZeroU32::new_unchecked(lo.wrapping_add(overflowed) & HIGH_MASK) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_u64_parts() { + // Two distinct bit patterns per low/high component + let value: u64 = 0x7FFF_FFFF_0000_000C; + + assert_eq!(IdentifierMask::get_low(value), 0x0000_000C); + assert_eq!(IdentifierMask::get_high(value), 0x7FFF_FFFF); + } + + #[test] + fn extract_kind() { + // All bits are ones. + let high: u32 = 0xFFFF_FFFF; + + assert_eq!( + IdentifierMask::extract_kind_from_high(high), + IdKind::Placeholder + ); + + // Second and second to last bits are ones. + let high: u32 = 0x4000_0002; + + assert_eq!(IdentifierMask::extract_kind_from_high(high), IdKind::Entity); + } + + #[test] + fn extract_high_value() { + // All bits are ones. + let high: u32 = 0xFFFF_FFFF; + + // Excludes the most significant bit as that is a flag bit. + assert_eq!(IdentifierMask::extract_value_from_high(high), 0x7FFF_FFFF); + + // Start bit and end bit are ones. + let high: u32 = 0x8000_0001; + + assert_eq!(IdentifierMask::extract_value_from_high(high), 0x0000_0001); + + // Classic bit pattern. + let high: u32 = 0xDEAD_BEEF; + + assert_eq!(IdentifierMask::extract_value_from_high(high), 0x5EAD_BEEF); + } + + #[test] + fn pack_kind_bits() { + // All bits are ones expect the most significant bit, which is zero + let high: u32 = 0x7FFF_FFFF; + + assert_eq!( + IdentifierMask::pack_kind_into_high(high, IdKind::Placeholder), + 0xFFFF_FFFF + ); + + // Arbitrary bit pattern + let high: u32 = 0x00FF_FF00; + + assert_eq!( + IdentifierMask::pack_kind_into_high(high, IdKind::Entity), + // Remains unchanged as before + 0x00FF_FF00 + ); + + // Bit pattern that almost spells a word + let high: u32 = 0x40FF_EEEE; + + assert_eq!( + IdentifierMask::pack_kind_into_high(high, IdKind::Placeholder), + 0xC0FF_EEEE // Milk and no sugar, please. + ); + } + + #[test] + fn pack_into_u64() { + let high: u32 = 0x7FFF_FFFF; + let low: u32 = 0x0000_00CC; + + assert_eq!( + IdentifierMask::pack_into_u64(low, high), + 0x7FFF_FFFF_0000_00CC + ); + } + + #[test] + fn incrementing_masked_nonzero_high_is_safe() { + // Adding from lowest value with lowest to highest increment + // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK + assert_eq!( + NonZeroU32::MIN, + IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 0) + ); + assert_eq!( + NonZeroU32::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 1) + ); + assert_eq!( + NonZeroU32::new(3).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, 2) + ); + assert_eq!( + NonZeroU32::MIN, + IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, HIGH_MASK) + ); + assert_eq!( + NonZeroU32::MIN, + IdentifierMask::inc_masked_high_by(NonZeroU32::MIN, u32::MAX) + ); + // Adding from absolute highest value with lowest to highest increment + // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK + assert_eq!( + NonZeroU32::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 0) + ); + assert_eq!( + NonZeroU32::MIN, + IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 1) + ); + assert_eq!( + NonZeroU32::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, 2) + ); + assert_eq!( + NonZeroU32::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, HIGH_MASK) + ); + assert_eq!( + NonZeroU32::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::MAX, u32::MAX) + ); + // Adding from actual highest value with lowest to highest increment + // No result should ever be greater than 0x7FFF_FFFF or HIGH_MASK + assert_eq!( + NonZeroU32::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 0) + ); + assert_eq!( + NonZeroU32::MIN, + IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 1) + ); + assert_eq!( + NonZeroU32::new(2).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), 2) + ); + assert_eq!( + NonZeroU32::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), HIGH_MASK) + ); + assert_eq!( + NonZeroU32::new(HIGH_MASK).unwrap(), + IdentifierMask::inc_masked_high_by(NonZeroU32::new(HIGH_MASK).unwrap(), u32::MAX) + ); + } +} diff --git a/crates/bevy_ecs/src/identifier/mod.rs b/crates/bevy_ecs/src/identifier/mod.rs new file mode 100644 index 0000000000000..d5d2b869c255c --- /dev/null +++ b/crates/bevy_ecs/src/identifier/mod.rs @@ -0,0 +1,241 @@ +//! A module for the unified [`Identifier`] ID struct, for use as a representation +//! of multiple types of IDs in a single, packed type. Allows for describing an [`crate::entity::Entity`], +//! or other IDs that can be packed and expressed within a `u64` sized type. +//! [`Identifier`]s cannot be created directly, only able to be converted from other +//! compatible IDs. +use self::{error::IdentifierError, kinds::IdKind, masks::IdentifierMask}; +use std::{hash::Hash, num::NonZeroU32}; + +pub mod error; +pub(crate) mod kinds; +pub(crate) mod masks; + +/// A unified identifier for all entity and similar IDs. +/// Has the same size as a `u64` integer, but the layout is split between a 32-bit low +/// segment, a 31-bit high segment, and the significant bit reserved as type flags to denote +/// entity kinds. +#[derive(Debug, Clone, Copy)] +// Alignment repr necessary to allow LLVM to better output +// optimised codegen for `to_bits`, `PartialEq` and `Ord`. +#[repr(C, align(8))] +pub struct Identifier { + // Do not reorder the fields here. The ordering is explicitly used by repr(C) + // to make this struct equivalent to a u64. + #[cfg(target_endian = "little")] + low: u32, + high: NonZeroU32, + #[cfg(target_endian = "big")] + low: u32, +} + +impl Identifier { + /// Construct a new [`Identifier`]. The `high` parameter is masked with the + /// `kind` so to pack the high value and bit flags into the same field. + #[inline(always)] + pub const fn new(low: u32, high: u32, kind: IdKind) -> Result { + // the high bits are masked to cut off the most significant bit + // as these are used for the type flags. This means that the high + // portion is only 31 bits, but this still provides 2^31 + // values/kinds/ids that can be stored in this segment. + let masked_value = IdentifierMask::extract_value_from_high(high); + + let packed_high = IdentifierMask::pack_kind_into_high(masked_value, kind); + + // If the packed high component ends up being zero, that means that we tried + // to initialise an Identifier into an invalid state. + if packed_high == 0 { + Err(IdentifierError::InvalidIdentifier) + } else { + // SAFETY: The high value has been checked to ensure it is never + // zero. + unsafe { + Ok(Self { + low, + high: NonZeroU32::new_unchecked(packed_high), + }) + } + } + } + + /// Returns the value of the low segment of the [`Identifier`]. + #[inline(always)] + pub const fn low(self) -> u32 { + self.low + } + + /// Returns the value of the high segment of the [`Identifier`]. This + /// does not apply any masking. + #[inline(always)] + pub const fn high(self) -> NonZeroU32 { + self.high + } + + /// Returns the masked value of the high segment of the [`Identifier`]. + /// Does not include the flag bits. + #[inline(always)] + pub const fn masked_high(self) -> u32 { + IdentifierMask::extract_value_from_high(self.high.get()) + } + + /// Returns the kind of [`Identifier`] from the high segment. + #[inline(always)] + pub const fn kind(self) -> IdKind { + IdentifierMask::extract_kind_from_high(self.high.get()) + } + + /// Convert the [`Identifier`] into a `u64`. + #[inline(always)] + pub const fn to_bits(self) -> u64 { + IdentifierMask::pack_into_u64(self.low, self.high.get()) + } + + /// Convert a `u64` into an [`Identifier`]. + /// + /// # Panics + /// + /// This method will likely panic if given `u64` values that did not come from [`Identifier::to_bits`]. + #[inline(always)] + pub const fn from_bits(value: u64) -> Self { + let id = Self::try_from_bits(value); + + match id { + Ok(id) => id, + Err(_) => panic!("Attempted to initialise invalid bits as an id"), + } + } + + /// Convert a `u64` into an [`Identifier`]. + /// + /// This method is the fallible counterpart to [`Identifier::from_bits`]. + #[inline(always)] + pub const fn try_from_bits(value: u64) -> Result { + let high = NonZeroU32::new(IdentifierMask::get_high(value)); + + match high { + Some(high) => Ok(Self { + low: IdentifierMask::get_low(value), + high, + }), + None => Err(IdentifierError::InvalidIdentifier), + } + } +} + +// By not short-circuiting in comparisons, we get better codegen. +// See +impl PartialEq for Identifier { + #[inline] + fn eq(&self, other: &Self) -> bool { + // By using `to_bits`, the codegen can be optimised out even + // further potentially. Relies on the correct alignment/field + // order of `Entity`. + self.to_bits() == other.to_bits() + } +} + +impl Eq for Identifier {} + +// The derive macro codegen output is not optimal and can't be optimised as well +// by the compiler. This impl resolves the issue of non-optimal codegen by relying +// on comparing against the bit representation of `Entity` instead of comparing +// the fields. The result is then LLVM is able to optimise the codegen for Entity +// far beyond what the derive macro can. +// See +impl PartialOrd for Identifier { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + // Make use of our `Ord` impl to ensure optimal codegen output + Some(self.cmp(other)) + } +} + +// The derive macro codegen output is not optimal and can't be optimised as well +// by the compiler. This impl resolves the issue of non-optimal codegen by relying +// on comparing against the bit representation of `Entity` instead of comparing +// the fields. The result is then LLVM is able to optimise the codegen for Entity +// far beyond what the derive macro can. +// See +impl Ord for Identifier { + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // This will result in better codegen for ordering comparisons, plus + // avoids pitfalls with regards to macro codegen relying on property + // position when we want to compare against the bit representation. + self.to_bits().cmp(&other.to_bits()) + } +} + +impl Hash for Identifier { + #[inline] + fn hash(&self, state: &mut H) { + self.to_bits().hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn id_construction() { + let id = Identifier::new(12, 55, IdKind::Entity).unwrap(); + + assert_eq!(id.low(), 12); + assert_eq!(id.high().get(), 55); + assert_eq!( + IdentifierMask::extract_kind_from_high(id.high().get()), + IdKind::Entity + ); + } + + #[test] + fn from_bits() { + // This high value should correspond to the max high() value + // and also Entity flag. + let high = 0x7FFFFFFF; + let low = 0xC; + let bits: u64 = high << u32::BITS | low; + + let id = Identifier::try_from_bits(bits).unwrap(); + + assert_eq!(id.to_bits(), 0x7FFFFFFF0000000C); + assert_eq!(id.low(), low as u32); + assert_eq!(id.high().get(), 0x7FFFFFFF); + assert_eq!( + IdentifierMask::extract_kind_from_high(id.high().get()), + IdKind::Entity + ); + } + + #[rustfmt::skip] + #[test] + fn id_comparison() { + // This is intentionally testing `lt` and `ge` as separate functions. + #![allow(clippy::nonminimal_bool)] + + assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() == Identifier::new(123, 456, IdKind::Entity).unwrap()); + assert!(Identifier::new(123, 456, IdKind::Placeholder).unwrap() == Identifier::new(123, 456, IdKind::Placeholder).unwrap()); + assert!(Identifier::new(123, 789, IdKind::Entity).unwrap() != Identifier::new(123, 456, IdKind::Entity).unwrap()); + assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() != Identifier::new(123, 789, IdKind::Entity).unwrap()); + assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() != Identifier::new(456, 123, IdKind::Entity).unwrap()); + assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() != Identifier::new(123, 456, IdKind::Placeholder).unwrap()); + + // ordering is by flag then high then by low + + assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() >= Identifier::new(123, 456, IdKind::Entity).unwrap()); + assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() <= Identifier::new(123, 456, IdKind::Entity).unwrap()); + assert!(!(Identifier::new(123, 456, IdKind::Entity).unwrap() < Identifier::new(123, 456, IdKind::Entity).unwrap())); + assert!(!(Identifier::new(123, 456, IdKind::Entity).unwrap() > Identifier::new(123, 456, IdKind::Entity).unwrap())); + + assert!(Identifier::new(9, 1, IdKind::Entity).unwrap() < Identifier::new(1, 9, IdKind::Entity).unwrap()); + assert!(Identifier::new(1, 9, IdKind::Entity).unwrap() > Identifier::new(9, 1, IdKind::Entity).unwrap()); + + assert!(Identifier::new(9, 1, IdKind::Entity).unwrap() < Identifier::new(9, 1, IdKind::Placeholder).unwrap()); + assert!(Identifier::new(1, 9, IdKind::Placeholder).unwrap() > Identifier::new(1, 9, IdKind::Entity).unwrap()); + + assert!(Identifier::new(1, 1, IdKind::Entity).unwrap() < Identifier::new(2, 1, IdKind::Entity).unwrap()); + assert!(Identifier::new(1, 1, IdKind::Entity).unwrap() <= Identifier::new(2, 1, IdKind::Entity).unwrap()); + assert!(Identifier::new(2, 2, IdKind::Entity).unwrap() > Identifier::new(1, 2, IdKind::Entity).unwrap()); + assert!(Identifier::new(2, 2, IdKind::Entity).unwrap() >= Identifier::new(1, 2, IdKind::Entity).unwrap()); + } +} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 4ffd78a29b3f1..0260860c75e27 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -10,6 +10,7 @@ pub mod change_detection; pub mod component; pub mod entity; pub mod event; +pub mod identifier; pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; @@ -27,7 +28,9 @@ pub use bevy_ptr as ptr; pub mod prelude { #[doc(hidden)] #[cfg(feature = "bevy_reflect")] - pub use crate::reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}; + pub use crate::reflect::{ + AppTypeRegistry, ReflectComponent, ReflectFromWorld, ReflectResource, + }; #[doc(hidden)] pub use crate::{ bundle::Bundle, @@ -35,12 +38,12 @@ pub mod prelude { component::Component, entity::Entity, event::{Event, EventReader, EventWriter, Events}, - query::{Added, AnyOf, Changed, Has, Or, QueryState, With, Without}, + query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, removal_detection::RemovedComponents, schedule::{ apply_deferred, apply_state_transition, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, OnExit, - OnTransition, Schedule, Schedules, State, States, SystemSet, + OnTransition, Schedule, Schedules, State, StateTransitionEvent, States, SystemSet, }, system::{ Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, @@ -53,7 +56,32 @@ pub mod prelude { pub use bevy_utils::all_tuples; /// A specialized hashmap type with Key of [`TypeId`] -type TypeIdMap = rustc_hash::FxHashMap; +type TypeIdMap = + std::collections::HashMap>; + +#[doc(hidden)] +#[derive(Default)] +struct NoOpTypeIdHasher(u64); + +// TypeId already contains a high-quality hash, so skip re-hashing that hash. +impl std::hash::Hasher for NoOpTypeIdHasher { + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, bytes: &[u8]) { + // This will never be called: TypeId always just calls write_u64 once! + // This is a known trick and unlikely to change, but isn't officially guaranteed. + // Don't break applications (slower fallback, just check in test): + self.0 = bytes.iter().fold(self.0, |hash, b| { + hash.rotate_left(8).wrapping_add(*b as u64) + }); + } + + fn write_u64(&mut self, i: u64) { + self.0 = i; + } +} #[cfg(test)] mod tests { @@ -69,6 +97,7 @@ mod tests { world::{EntityRef, Mut, World}, }; use bevy_tasks::{ComputeTaskPool, TaskPool}; + use std::num::NonZeroU32; use std::{ any::TypeId, marker::PhantomData, @@ -1559,7 +1588,7 @@ mod tests { let e4 = world_b.spawn(A(4)).id(); assert_eq!( e4, - Entity::new(3, 0), + Entity::from_raw(3), "new entity is created immediately after world_a's max entity" ); assert!(world_b.get::(e1).is_none()); @@ -1590,7 +1619,8 @@ mod tests { "spawning into existing `world_b` entities works" ); - let e4_mismatched_generation = Entity::new(3, 1); + let e4_mismatched_generation = + Entity::from_raw_and_generation(3, NonZeroU32::new(2).unwrap()); assert!( world_b.get_or_spawn(e4_mismatched_generation).is_none(), "attempting to spawn on top of an entity with a mismatched entity generation fails" @@ -1606,7 +1636,7 @@ mod tests { "failed mismatched spawn doesn't change existing entity" ); - let high_non_existent_entity = Entity::new(6, 0); + let high_non_existent_entity = Entity::from_raw(6); world_b .get_or_spawn(high_non_existent_entity) .unwrap() @@ -1617,7 +1647,7 @@ mod tests { "inserting into newly allocated high / non-continuous entity id works" ); - let high_non_existent_but_reserved_entity = Entity::new(5, 0); + let high_non_existent_but_reserved_entity = Entity::from_raw(5); assert!( world_b.get_entity(high_non_existent_but_reserved_entity).is_none(), "entities between high-newly allocated entity and continuous block of existing entities don't exist" @@ -1633,10 +1663,10 @@ mod tests { assert_eq!( reserved_entities, vec![ - Entity::new(5, 0), - Entity::new(4, 0), - Entity::new(7, 0), - Entity::new(8, 0), + Entity::from_raw(5), + Entity::from_raw(4), + Entity::from_raw(7), + Entity::from_raw(8), ], "space between original entities and high entities is used for new entity ids" ); @@ -1685,7 +1715,7 @@ mod tests { let e0 = world.spawn(A(0)).id(); let e1 = Entity::from_raw(1); let e2 = world.spawn_empty().id(); - let invalid_e2 = Entity::new(e2.index(), 1); + let invalid_e2 = Entity::from_raw_and_generation(e2.index(), NonZeroU32::new(2).unwrap()); let values = vec![(e0, (B(0), C)), (e1, (B(1), C)), (invalid_e2, (B(2), C))]; @@ -1724,6 +1754,23 @@ mod tests { ); } + #[test] + fn fast_typeid_hash() { + struct Hasher; + + impl std::hash::Hasher for Hasher { + fn finish(&self) -> u64 { + 0 + } + fn write(&mut self, _: &[u8]) { + panic!("Hashing of std::any::TypeId changed"); + } + fn write_u64(&mut self, _: u64) {} + } + + std::hash::Hash::hash(&TypeId::of::<()>(), &mut Hasher); + } + #[derive(Component)] struct ComponentA(u32); diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 808d31d2c48e7..d7253f073ed7f 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -157,6 +157,12 @@ impl Access { self.writes_all } + /// Removes all writes. + pub fn clear_writes(&mut self) { + self.writes_all = false; + self.writes.clear(); + } + /// Removes all accesses. pub fn clear(&mut self) { self.reads_all = false; @@ -198,6 +204,29 @@ impl Access { && other.writes.is_disjoint(&self.reads_and_writes) } + /// Returns `true` if the set is a subset of another, i.e. `other` contains + /// at least all the values in `self`. + pub fn is_subset(&self, other: &Access) -> bool { + if self.writes_all { + return other.writes_all; + } + + if other.writes_all { + return true; + } + + if self.reads_all { + return other.reads_all; + } + + if other.reads_all { + return self.writes.is_subset(&other.writes); + } + + self.reads_and_writes.is_subset(&other.reads_and_writes) + && self.writes.is_subset(&other.writes) + } + /// Returns a vector of elements that the access and `other` cannot access at the same time. pub fn get_conflicts(&self, other: &Access) -> Vec { let mut conflicts = FixedBitSet::default(); @@ -267,16 +296,18 @@ impl Access { /// See comments the [`WorldQuery`](super::WorldQuery) impls of [`AnyOf`](super::AnyOf)/`Option`/[`Or`](super::Or) for more information. #[derive(Debug, Clone, Eq, PartialEq)] pub struct FilteredAccess { - access: Access, + pub(crate) access: Access, + pub(crate) required: FixedBitSet, // An array of filter sets to express `With` or `Without` clauses in disjunctive normal form, for example: `Or<(With, With)>`. // Filters like `(With, Or<(With, Without)>` are expanded into `Or<((With, With), (With, Without))>`. - filter_sets: Vec>, + pub(crate) filter_sets: Vec>, } impl Default for FilteredAccess { fn default() -> Self { Self { access: Access::default(), + required: FixedBitSet::default(), filter_sets: vec![AccessFilters::default()], } } @@ -306,15 +337,23 @@ impl FilteredAccess { /// Adds access to the element given by `index`. pub fn add_read(&mut self, index: T) { self.access.add_read(index.clone()); + self.add_required(index.clone()); self.and_with(index); } /// Adds exclusive access to the element given by `index`. pub fn add_write(&mut self, index: T) { self.access.add_write(index.clone()); + self.add_required(index.clone()); self.and_with(index); } + fn add_required(&mut self, index: T) { + let index = index.sparse_set_index(); + self.required.grow(index + 1); + self.required.insert(index); + } + /// Adds a `With` filter: corresponds to a conjunction (AND) operation. /// /// Suppose we begin with `Or<(With, With)>`, which is represented by an array of two `AccessFilter` instances. @@ -391,6 +430,7 @@ impl FilteredAccess { /// `Or<((With, With), (With, Without), (Without, With), (Without, Without))>`. pub fn extend(&mut self, other: &FilteredAccess) { self.access.extend(&other.access); + self.required.union_with(&other.required); // We can avoid allocating a new array of bitsets if `other` contains just a single set of filters: // in this case we can short-circuit by performing an in-place union for each bitset. @@ -423,12 +463,18 @@ impl FilteredAccess { pub fn write_all(&mut self) { self.access.write_all(); } + + /// Returns `true` if the set is a subset of another, i.e. `other` contains + /// at least all the values in `self`. + pub fn is_subset(&self, other: &FilteredAccess) -> bool { + self.required.is_subset(&other.required) && self.access().is_subset(other.access()) + } } #[derive(Clone, Eq, PartialEq)] -struct AccessFilters { - with: FixedBitSet, - without: FixedBitSet, +pub(crate) struct AccessFilters { + pub(crate) with: FixedBitSet, + pub(crate) without: FixedBitSet, _index_type: PhantomData, } diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs new file mode 100644 index 0000000000000..67db644a4fcb4 --- /dev/null +++ b/crates/bevy_ecs/src/query/builder.rs @@ -0,0 +1,401 @@ +use std::marker::PhantomData; + +use crate::{component::ComponentId, prelude::*}; + +use super::{FilteredAccess, QueryData, QueryFilter}; + +/// Builder struct to create [`QueryState`] instances at runtime. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Component)] +/// # struct A; +/// # +/// # #[derive(Component)] +/// # struct B; +/// # +/// # #[derive(Component)] +/// # struct C; +/// # +/// let mut world = World::new(); +/// let entity_a = world.spawn((A, B)).id(); +/// let entity_b = world.spawn((A, C)).id(); +/// +/// // Instantiate the builder using the type signature of the iterator you will consume +/// let mut query = QueryBuilder::<(Entity, &B)>::new(&mut world) +/// // Add additional terms through builder methods +/// .with::() +/// .without::() +/// .build(); +/// +/// // Consume the QueryState +/// let (entity, b) = query.single(&world); +///``` +pub struct QueryBuilder<'w, D: QueryData = (), F: QueryFilter = ()> { + access: FilteredAccess, + world: &'w mut World, + or: bool, + first: bool, + _marker: PhantomData<(D, F)>, +} + +impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { + /// Creates a new builder with the accesses required for `Q` and `F` + pub fn new(world: &'w mut World) -> Self { + let fetch_state = D::init_state(world); + let filter_state = F::init_state(world); + + let mut access = FilteredAccess::default(); + D::update_component_access(&fetch_state, &mut access); + + // Use a temporary empty FilteredAccess for filters. This prevents them from conflicting with the + // main Query's `fetch_state` access. Filters are allowed to conflict with the main query fetch + // because they are evaluated *before* a specific reference is constructed. + let mut filter_access = FilteredAccess::default(); + F::update_component_access(&filter_state, &mut filter_access); + + // Merge the temporary filter access with the main access. This ensures that filter access is + // properly considered in a global "cross-query" context (both within systems and across systems). + access.extend(&filter_access); + + Self { + access, + world, + or: false, + first: false, + _marker: PhantomData, + } + } + + /// Returns a reference to the world passed to [`Self::new`]. + pub fn world(&self) -> &World { + self.world + } + + /// Returns a mutable reference to the world passed to [`Self::new`]. + pub fn world_mut(&mut self) -> &mut World { + self.world + } + + /// Adds access to self's underlying [`FilteredAccess`] respecting [`Self::or`] and [`Self::and`] + pub fn extend_access(&mut self, mut access: FilteredAccess) { + if self.or { + if self.first { + access.required.clear(); + self.access.extend(&access); + self.first = false; + } else { + self.access.append_or(&access); + } + } else { + self.access.extend(&access); + } + } + + /// Adds accesses required for `T` to self. + pub fn data(&mut self) -> &mut Self { + let state = T::init_state(self.world); + let mut access = FilteredAccess::default(); + T::update_component_access(&state, &mut access); + self.extend_access(access); + self + } + + /// Adds filter from `T` to self. + pub fn filter(&mut self) -> &mut Self { + let state = T::init_state(self.world); + let mut access = FilteredAccess::default(); + T::update_component_access(&state, &mut access); + self.extend_access(access); + self + } + + /// Adds [`With`] to the [`FilteredAccess`] of self. + pub fn with(&mut self) -> &mut Self { + self.filter::>(); + self + } + + /// Adds [`With`] to the [`FilteredAccess`] of self from a runtime [`ComponentId`]. + pub fn with_id(&mut self, id: ComponentId) -> &mut Self { + let mut access = FilteredAccess::default(); + access.and_with(id); + self.extend_access(access); + self + } + + /// Adds [`Without`] to the [`FilteredAccess`] of self. + pub fn without(&mut self) -> &mut Self { + self.filter::>(); + self + } + + /// Adds [`Without`] to the [`FilteredAccess`] of self from a runtime [`ComponentId`]. + pub fn without_id(&mut self, id: ComponentId) -> &mut Self { + let mut access = FilteredAccess::default(); + access.and_without(id); + self.extend_access(access); + self + } + + /// Adds `&T` to the [`FilteredAccess`] of self. + pub fn ref_id(&mut self, id: ComponentId) -> &mut Self { + self.with_id(id); + self.access.add_read(id); + self + } + + /// Adds `&mut T` to the [`FilteredAccess`] of self. + pub fn mut_id(&mut self, id: ComponentId) -> &mut Self { + self.with_id(id); + self.access.add_write(id); + self + } + + /// Takes a function over mutable access to a [`QueryBuilder`], calls that function + /// on an empty builder and then adds all accesses from that builder to self as optional. + pub fn optional(&mut self, f: impl Fn(&mut QueryBuilder)) -> &mut Self { + let mut builder = QueryBuilder::new(self.world); + f(&mut builder); + self.access.extend_access(builder.access()); + self + } + + /// Takes a function over mutable access to a [`QueryBuilder`], calls that function + /// on an empty builder and then adds all accesses from that builder to self. + /// + /// Primarily used when inside a [`Self::or`] closure to group several terms. + pub fn and(&mut self, f: impl Fn(&mut QueryBuilder)) -> &mut Self { + let mut builder = QueryBuilder::new(self.world); + f(&mut builder); + let access = builder.access().clone(); + self.extend_access(access); + self + } + + /// Takes a function over mutable access to a [`QueryBuilder`], calls that function + /// on an empty builder, all accesses added to that builder will become terms in an or expression. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct A; + /// # + /// # #[derive(Component)] + /// # struct B; + /// # + /// # let mut world = World::new(); + /// # + /// QueryBuilder::::new(&mut world).or(|builder| { + /// builder.with::(); + /// builder.with::(); + /// }); + /// // is equivalent to + /// QueryBuilder::::new(&mut world).filter::, With)>>(); + /// ``` + pub fn or(&mut self, f: impl Fn(&mut QueryBuilder)) -> &mut Self { + let mut builder = QueryBuilder::new(self.world); + builder.or = true; + builder.first = true; + f(&mut builder); + self.access.extend(builder.access()); + self + } + + /// Returns a reference to the the [`FilteredAccess`] that will be provided to the built [`Query`]. + pub fn access(&self) -> &FilteredAccess { + &self.access + } + + /// Transmute the existing builder adding required accesses. + /// This will maintain all exisiting accesses. + /// + /// If including a filter type see [`Self::transmute_filtered`] + pub fn transmute(&mut self) -> &mut QueryBuilder<'w, NewD> { + self.transmute_filtered::() + } + + /// Transmute the existing builder adding required accesses. + /// This will maintain all existing accesses. + pub fn transmute_filtered( + &mut self, + ) -> &mut QueryBuilder<'w, NewD, NewF> { + let mut fetch_state = NewD::init_state(self.world); + let filter_state = NewF::init_state(self.world); + + NewD::set_access(&mut fetch_state, &self.access); + + let mut access = FilteredAccess::default(); + NewD::update_component_access(&fetch_state, &mut access); + NewF::update_component_access(&filter_state, &mut access); + + self.extend_access(access); + // SAFETY: + // - We have included all required acceses for NewQ and NewF + // - The layout of all QueryBuilder instances is the same + unsafe { std::mem::transmute(self) } + } + + /// Create a [`QueryState`] with the accesses of the builder. + /// + /// Takes `&mut self` to access the innner world reference while initializing + /// state for the new [`QueryState`] + pub fn build(&mut self) -> QueryState { + QueryState::::from_builder(self) + } +} + +#[cfg(test)] +mod tests { + use crate as bevy_ecs; + use crate::prelude::*; + use crate::world::FilteredEntityRef; + + use super::QueryBuilder; + + #[derive(Component, PartialEq, Debug)] + struct A(usize); + + #[derive(Component, PartialEq, Debug)] + struct B(usize); + + #[derive(Component, PartialEq, Debug)] + struct C(usize); + + #[test] + fn builder_with_without_static() { + let mut world = World::new(); + let entity_a = world.spawn((A(0), B(0))).id(); + let entity_b = world.spawn((A(0), C(0))).id(); + + let mut query_a = QueryBuilder::::new(&mut world) + .with::() + .without::() + .build(); + assert_eq!(entity_a, query_a.single(&world)); + + let mut query_b = QueryBuilder::::new(&mut world) + .with::() + .without::() + .build(); + assert_eq!(entity_b, query_b.single(&world)); + } + + #[test] + fn builder_with_without_dynamic() { + let mut world = World::new(); + let entity_a = world.spawn((A(0), B(0))).id(); + let entity_b = world.spawn((A(0), C(0))).id(); + let component_id_a = world.init_component::(); + let component_id_b = world.init_component::(); + let component_id_c = world.init_component::(); + + let mut query_a = QueryBuilder::::new(&mut world) + .with_id(component_id_a) + .without_id(component_id_c) + .build(); + assert_eq!(entity_a, query_a.single(&world)); + + let mut query_b = QueryBuilder::::new(&mut world) + .with_id(component_id_a) + .without_id(component_id_b) + .build(); + assert_eq!(entity_b, query_b.single(&world)); + } + + #[test] + fn builder_or() { + let mut world = World::new(); + world.spawn((A(0), B(0))); + world.spawn(B(0)); + world.spawn(C(0)); + + let mut query_a = QueryBuilder::::new(&mut world) + .or(|builder| { + builder.with::(); + builder.with::(); + }) + .build(); + assert_eq!(2, query_a.iter(&world).count()); + + let mut query_b = QueryBuilder::::new(&mut world) + .or(|builder| { + builder.with::(); + builder.without::(); + }) + .build(); + dbg!(&query_b.component_access); + assert_eq!(2, query_b.iter(&world).count()); + + let mut query_c = QueryBuilder::::new(&mut world) + .or(|builder| { + builder.with::(); + builder.with::(); + builder.with::(); + }) + .build(); + assert_eq!(3, query_c.iter(&world).count()); + } + + #[test] + fn builder_transmute() { + let mut world = World::new(); + world.spawn(A(0)); + world.spawn((A(1), B(0))); + let mut query = QueryBuilder::<()>::new(&mut world) + .with::() + .transmute::<&A>() + .build(); + + query.iter(&world).for_each(|a| assert_eq!(a.0, 1)); + } + + #[test] + fn builder_static_components() { + let mut world = World::new(); + let entity = world.spawn((A(0), B(1))).id(); + + let mut query = QueryBuilder::::new(&mut world) + .data::<&A>() + .data::<&B>() + .build(); + + let entity_ref = query.single(&world); + + assert_eq!(entity, entity_ref.id()); + + let a = entity_ref.get::().unwrap(); + let b = entity_ref.get::().unwrap(); + + assert_eq!(0, a.0); + assert_eq!(1, b.0); + } + + #[test] + fn builder_dynamic_components() { + let mut world = World::new(); + let entity = world.spawn((A(0), B(1))).id(); + let component_id_a = world.init_component::(); + let component_id_b = world.init_component::(); + + let mut query = QueryBuilder::::new(&mut world) + .ref_id(component_id_a) + .ref_id(component_id_b) + .build(); + + let entity_ref = query.single(&world); + + assert_eq!(entity, entity_ref.id()); + + let a = entity_ref.get_by_id(component_id_a).unwrap(); + let b = entity_ref.get_by_id(component_id_b).unwrap(); + + // SAFETY: We set these pointers to point to these components + unsafe { + assert_eq!(0, a.deref::().0); + assert_eq!(1, b.deref::().0); + } + } +} diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index b384b18e27f0b..5e83637d33019 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,11 +1,14 @@ use crate::{ - archetype::{Archetype, ArchetypeComponentId}, + archetype::Archetype, change_detection::{Ticks, TicksMut}, component::{Component, ComponentId, ComponentStorage, StorageType, Tick}, entity::Entity, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{ComponentSparseSet, Table, TableRow}, - world::{unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, Mut, Ref, World}, + world::{ + unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, FilteredEntityMut, + FilteredEntityRef, Mut, Ref, World, + }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::all_tuples; @@ -320,15 +323,12 @@ unsafe impl WorldQuery for Entity { fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} - fn update_archetype_component_access( - _state: &Self::State, - _archetype: &Archetype, - _access: &mut Access, - ) { - } - fn init_state(_world: &mut World) {} + fn get_state(_world: &World) -> Option<()> { + Some(()) + } + fn matches_component_set( _state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool, @@ -402,18 +402,12 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { access.read_all(); } - fn update_archetype_component_access( - _state: &Self::State, - archetype: &Archetype, - access: &mut Access, - ) { - for component_id in archetype.components() { - access.add_read(archetype.get_archetype_component_id(component_id).unwrap()); - } - } - fn init_state(_world: &mut World) {} + fn get_state(_world: &World) -> Option<()> { + Some(()) + } + fn matches_component_set( _state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool, @@ -484,18 +478,12 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { access.write_all(); } - fn update_archetype_component_access( - _state: &Self::State, - archetype: &Archetype, - access: &mut Access, - ) { - for component_id in archetype.components() { - access.add_write(archetype.get_archetype_component_id(component_id).unwrap()); - } - } - fn init_state(_world: &mut World) {} + fn get_state(_world: &World) -> Option<()> { + Some(()) + } + fn matches_component_set( _state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool, @@ -509,6 +497,218 @@ unsafe impl<'a> QueryData for EntityMut<'a> { type ReadOnly = EntityRef<'a>; } +/// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` +unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { + type Fetch<'w> = (UnsafeWorldCell<'w>, Access); + type Item<'w> = FilteredEntityRef<'w>; + type State = FilteredAccess; + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + const IS_DENSE: bool = false; + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + let mut access = Access::default(); + access.read_all(); + (world, access) + } + + #[inline] + unsafe fn set_archetype<'w>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + archetype: &'w Archetype, + _table: &Table, + ) { + let mut access = Access::default(); + state.access.reads().for_each(|id| { + if archetype.contains(id) { + access.add_read(id); + } + }); + fetch.1 = access; + } + + #[inline] + unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + let mut access = Access::default(); + state.access.reads().for_each(|id| { + if table.has_column(id) { + access.add_read(id); + } + }); + fetch.1 = access; + } + + #[inline] + fn set_access<'w>(state: &mut Self::State, access: &FilteredAccess) { + *state = access.clone(); + state.access_mut().clear_writes(); + } + + #[inline(always)] + unsafe fn fetch<'w>( + (world, access): &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + // SAFETY: `fetch` must be called with an entity that exists in the world + let cell = world.get_entity(entity).debug_checked_unwrap(); + // SAFETY: mutable access to every component has been registered. + FilteredEntityRef::new(cell, access.clone()) + } + + fn update_component_access( + state: &Self::State, + filtered_access: &mut FilteredAccess, + ) { + assert!( + filtered_access.access().is_compatible(&state.access), + "FilteredEntityRef conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.", + ); + filtered_access.access.extend(&state.access); + } + + fn init_state(_world: &mut World) -> Self::State { + FilteredAccess::default() + } + + fn get_state(_world: &World) -> Option { + Some(FilteredAccess::default()) + } + + 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<'a> QueryData for FilteredEntityRef<'a> { + type ReadOnly = Self; +} + +/// SAFETY: Access is read-only. +unsafe impl ReadOnlyQueryData for FilteredEntityRef<'_> {} + +/// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` +unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { + type Fetch<'w> = (UnsafeWorldCell<'w>, Access); + type Item<'w> = FilteredEntityMut<'w>; + type State = FilteredAccess; + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + const IS_DENSE: bool = false; + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + let mut access = Access::default(); + access.write_all(); + (world, access) + } + + #[inline] + unsafe fn set_archetype<'w>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + archetype: &'w Archetype, + _table: &Table, + ) { + let mut access = Access::default(); + state.access.reads().for_each(|id| { + if archetype.contains(id) { + access.add_read(id); + } + }); + state.access.writes().for_each(|id| { + if archetype.contains(id) { + access.add_write(id); + } + }); + fetch.1 = access; + } + + #[inline] + unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + let mut access = Access::default(); + state.access.reads().for_each(|id| { + if table.has_column(id) { + access.add_read(id); + } + }); + state.access.writes().for_each(|id| { + if table.has_column(id) { + access.add_write(id); + } + }); + fetch.1 = access; + } + + #[inline] + fn set_access<'w>(state: &mut Self::State, access: &FilteredAccess) { + *state = access.clone(); + } + + #[inline(always)] + unsafe fn fetch<'w>( + (world, access): &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + // SAFETY: `fetch` must be called with an entity that exists in the world + let cell = world.get_entity(entity).debug_checked_unwrap(); + // SAFETY: mutable access to every component has been registered. + FilteredEntityMut::new(cell, access.clone()) + } + + fn update_component_access( + state: &Self::State, + filtered_access: &mut FilteredAccess, + ) { + assert!( + filtered_access.access().is_compatible(&state.access), + "FilteredEntityMut conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.", + ); + filtered_access.access.extend(&state.access); + } + + fn init_state(_world: &mut World) -> Self::State { + FilteredAccess::default() + } + + fn get_state(_world: &World) -> Option { + Some(FilteredAccess::default()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } +} + +/// SAFETY: access of `FilteredEntityRef` is a subset of `FilteredEntityMut` +unsafe impl<'a> QueryData for FilteredEntityMut<'a> { + type ReadOnly = FilteredEntityRef<'a>; +} + #[doc(hidden)] pub struct ReadFetch<'w, T> { // T::Storage = TableStorage @@ -628,20 +828,14 @@ unsafe impl WorldQuery for &T { access.add_read(component_id); } - fn update_archetype_component_access( - &component_id: &ComponentId, - archetype: &Archetype, - access: &mut Access, - ) { - if let Some(archetype_component_id) = archetype.get_archetype_component_id(component_id) { - access.add_read(archetype_component_id); - } - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &state: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -795,20 +989,14 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { access.add_read(component_id); } - fn update_archetype_component_access( - &component_id: &ComponentId, - archetype: &Archetype, - access: &mut Access, - ) { - if let Some(archetype_component_id) = archetype.get_archetype_component_id(component_id) { - access.add_read(archetype_component_id); - } - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &state: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -962,20 +1150,14 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { access.add_write(component_id); } - fn update_archetype_component_access( - &component_id: &ComponentId, - archetype: &Archetype, - access: &mut Access, - ) { - if let Some(archetype_component_id) = archetype.get_archetype_component_id(component_id) { - access.add_write(archetype_component_id); - } - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &state: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -1079,20 +1261,14 @@ unsafe impl WorldQuery for Option { access.extend_access(&intermediate); } - fn update_archetype_component_access( - state: &T::State, - archetype: &Archetype, - access: &mut Access, - ) { - if T::matches_component_set(state, &|id| archetype.contains(id)) { - T::update_archetype_component_access(state, archetype, access); - } - } - fn init_state(world: &mut World) -> T::State { T::init_state(world) } + fn get_state(world: &World) -> Option { + T::get_state(world) + } + fn matches_component_set( _state: &T::State, _set_contains_id: &impl Fn(ComponentId) -> bool, @@ -1114,6 +1290,16 @@ unsafe impl ReadOnlyQueryData for Option {} /// This can be used in a [`Query`](crate::system::Query) if you want to know whether or not entities /// have the component `T` but don't actually care about the component's value. /// +/// # Footguns +/// +/// Note that a `Query>` will match all existing entities. +/// Beware! Even if it matches all entities, it doesn't mean that `query.get(entity)` +/// will always return `Ok(bool)`. +/// +/// In the case of a non-existent entity, such as a despawned one, it will return `Err`. +/// A workaround is to replace `query.get(entity).unwrap()` by +/// `query.get(entity).unwrap_or_default()`. +/// /// # Examples /// /// ``` @@ -1221,17 +1407,14 @@ unsafe impl WorldQuery for Has { // Do nothing as presence of `Has` never affects whether two queries are disjoint } - fn update_archetype_component_access( - _state: &Self::State, - _archetype: &Archetype, - _access: &mut Access, - ) { - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( _state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool, @@ -1358,6 +1541,7 @@ macro_rules! impl_anytuple_fetch { _new_access.extend_access(&intermediate); } else { $name::update_component_access($name, &mut _new_access); + _new_access.required = _access.required.clone(); _not_first = true; } )* @@ -1365,19 +1549,14 @@ macro_rules! impl_anytuple_fetch { *_access = _new_access; } - fn update_archetype_component_access(state: &Self::State, _archetype: &Archetype, _access: &mut Access) { - let ($($name,)*) = state; - $( - if $name::matches_component_set($name, &|id| _archetype.contains(id)) { - $name::update_archetype_component_access($name, _archetype, _access); - } - )* - } - fn init_state(_world: &mut World) -> Self::State { ($($name::init_state(_world),)*) } + fn get_state(_world: &World) -> Option { + Some(($($name::get_state(_world)?,)*)) + } + fn matches_component_set(_state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { let ($($name,)*) = _state; false $(|| $name::matches_component_set($name, _set_contains_id))* @@ -1447,17 +1626,14 @@ unsafe impl WorldQuery for NopWorldQuery { fn update_component_access(_state: &D::State, _access: &mut FilteredAccess) {} - fn update_archetype_component_access( - _state: &D::State, - _archetype: &Archetype, - _access: &mut Access, - ) { - } - fn init_state(world: &mut World) -> Self::State { D::init_state(world) } + fn get_state(world: &World) -> Option { + D::get_state(world) + } + fn matches_component_set( state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -1517,15 +1693,12 @@ unsafe impl WorldQuery for PhantomData { fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} - fn update_archetype_component_access( - _state: &Self::State, - _archetype: &Archetype, - _access: &mut Access, - ) { - } - fn init_state(_world: &mut World) -> Self::State {} + fn get_state(_world: &World) -> Option { + Some(()) + } + fn matches_component_set( _state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool, diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 301ddfbd6aa6e..a8d225ab81173 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -1,8 +1,8 @@ use crate::{ - archetype::{Archetype, ArchetypeComponentId}, + archetype::Archetype, component::{Component, ComponentId, ComponentStorage, StorageType, Tick}, entity::Entity, - query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, + query::{DebugCheckedUnwrap, FilteredAccess, WorldQuery}, storage::{Column, ComponentSparseSet, Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -175,18 +175,14 @@ unsafe impl WorldQuery for With { access.and_with(id); } - #[inline] - fn update_archetype_component_access( - _state: &ComponentId, - _archetype: &Archetype, - _access: &mut Access, - ) { - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &id: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -287,18 +283,14 @@ unsafe impl WorldQuery for Without { access.and_without(id); } - #[inline] - fn update_archetype_component_access( - _state: &ComponentId, - _archetype: &Archetype, - _access: &mut Access, - ) { - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &id: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -449,6 +441,7 @@ macro_rules! impl_query_filter_tuple { _new_access.extend_access(&intermediate); } else { $filter::update_component_access($filter, &mut _new_access); + _new_access.required = access.required.clone(); _not_first = true; } )* @@ -456,15 +449,14 @@ macro_rules! impl_query_filter_tuple { *access = _new_access; } - fn update_archetype_component_access(state: &Self::State, archetype: &Archetype, access: &mut Access) { - let ($($filter,)*) = state; - $($filter::update_archetype_component_access($filter, archetype, access);)* - } - fn init_state(world: &mut World) -> Self::State { ($($filter::init_state(world),)*) } + fn get_state(world: &World) -> Option { + Some(($($filter::get_state(world)?,)*)) + } + fn matches_component_set(_state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { let ($($filter,)*) = _state; false $(|| $filter::matches_component_set($filter, _set_contains_id))* @@ -525,6 +517,36 @@ all_tuples!(impl_query_filter_tuple, 0, 15, F, S); /// are visible only after deferred operations are applied, /// typically at the end of the schedule iteration. /// +/// # Time complexity +/// +/// `Added` is not [`ArchetypeFilter`], which practically means that +/// if query (with `T` component filter) matches million entities, +/// `Added` filter will iterate over all of them even if none of them were just added. +/// +/// For example, these two systems are roughly equivalent in terms of performance: +/// +/// ``` +/// # use bevy_ecs::change_detection::{DetectChanges, Ref}; +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::query::Added; +/// # use bevy_ecs::system::Query; +/// # use bevy_ecs_macros::Component; +/// # #[derive(Component)] +/// # struct MyComponent; +/// # #[derive(Component)] +/// # struct Transform; +/// +/// fn system1(q: Query<&MyComponent, Added>) { +/// for item in &q { /* component added */ } +/// } +/// +/// fn system2(q: Query<(&MyComponent, Ref)>) { +/// for item in &q { +/// if item.1.is_added() { /* component added */ } +/// } +/// } +/// ``` +/// /// # Examples /// /// ``` @@ -647,21 +669,14 @@ unsafe impl WorldQuery for Added { access.add_read(id); } - #[inline] - fn update_archetype_component_access( - &id: &ComponentId, - archetype: &Archetype, - access: &mut Access, - ) { - if let Some(archetype_component_id) = archetype.get_archetype_component_id(id) { - access.add_read(archetype_component_id); - } - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &id: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, @@ -699,6 +714,37 @@ impl QueryFilter for Added { /// are visible only after deferred operations are applied, /// typically at the end of the schedule iteration. /// +/// # Time complexity +/// +/// `Changed` is not [`ArchetypeFilter`], which practically means that +/// if query (with `T` component filter) matches million entities, +/// `Changed` filter will iterate over all of them even if none of them were changed. +/// +/// For example, these two systems are roughly equivalent in terms of performance: +/// +/// ``` +/// # use bevy_ecs::change_detection::DetectChanges; +/// # use bevy_ecs::entity::Entity; +/// # use bevy_ecs::query::Changed; +/// # use bevy_ecs::system::Query; +/// # use bevy_ecs::world::Ref; +/// # use bevy_ecs_macros::Component; +/// # #[derive(Component)] +/// # struct MyComponent; +/// # #[derive(Component)] +/// # struct Transform; +/// +/// fn system1(q: Query<&MyComponent, Changed>) { +/// for item in &q { /* component changed */ } +/// } +/// +/// fn system2(q: Query<(&MyComponent, Ref)>) { +/// for item in &q { +/// if item.1.is_changed() { /* component changed */ } +/// } +/// } +/// ``` +/// /// # Examples /// /// ``` @@ -823,21 +869,14 @@ unsafe impl WorldQuery for Changed { access.add_read(id); } - #[inline] - fn update_archetype_component_access( - &id: &ComponentId, - archetype: &Archetype, - access: &mut Access, - ) { - if let Some(archetype_component_id) = archetype.get_archetype_component_id(id) { - access.add_read(archetype_component_id); - } - } - fn init_state(world: &mut World) -> ComponentId { world.init_component::() } + fn get_state(world: &World) -> Option { + world.component_id::() + } + fn matches_component_set( &id: &ComponentId, set_contains_id: &impl Fn(ComponentId) -> bool, diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 0b9950f4eccfb..e79371c0d7a6e 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -178,7 +178,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. if !F::filter_fetch( &mut self.cursor.filter, - archetype_entity.entity(), + archetype_entity.id(), archetype_entity.table_row(), ) { continue; @@ -188,7 +188,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let item = D::fetch( &mut self.cursor.fetch, - archetype_entity.entity(), + archetype_entity.id(), archetype_entity.table_row(), ); @@ -719,7 +719,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { let archetype_entity = self.archetype_entities.get_unchecked(index); Some(D::fetch( &mut self.fetch, - archetype_entity.entity(), + archetype_entity.id(), archetype_entity.table_row(), )) } @@ -817,7 +817,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { let archetype_entity = self.archetype_entities.get_unchecked(self.current_row); if !F::filter_fetch( &mut self.filter, - archetype_entity.entity(), + archetype_entity.id(), archetype_entity.table_row(), ) { self.current_row += 1; @@ -831,7 +831,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - fetch is only called once for each `archetype_entity`. let item = D::fetch( &mut self.fetch, - archetype_entity.entity(), + archetype_entity.id(), archetype_entity.table_row(), ); self.current_row += 1; diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 3beaf664ac33b..618c179f98570 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -1,6 +1,7 @@ //! Contains APIs for retrieving component data from the world. mod access; +mod builder; mod error; mod fetch; mod filter; @@ -11,6 +12,7 @@ mod world_query; pub use access::*; pub use bevy_ecs_macros::{QueryData, QueryFilter}; +pub use builder::*; pub use error::*; pub use fetch::*; pub use filter::*; diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 8da4e2fcca9c3..58f5db9a1c1a8 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -8,7 +8,7 @@ use crate::{ Access, BatchingStrategy, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, QueryIter, QueryParIter, }, - storage::TableId, + storage::{SparseSetIndex, TableId}, world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId}, }; #[cfg(feature = "trace")] @@ -17,8 +17,8 @@ use fixedbitset::FixedBitSet; use std::{any::TypeId, borrow::Borrow, fmt, mem::MaybeUninit}; use super::{ - NopWorldQuery, QueryComponentError, QueryData, QueryEntityError, QueryFilter, QueryManyIter, - QuerySingleError, ROQueryItem, + NopWorldQuery, QueryBuilder, QueryComponentError, QueryData, QueryEntityError, QueryFilter, + QueryManyIter, QuerySingleError, ROQueryItem, }; /// Provides scoped access to a [`World`] state according to a given [`QueryData`] and [`QueryFilter`]. @@ -138,6 +138,34 @@ impl QueryState { state } + /// Creates a new [`QueryState`] from a given [`QueryBuilder`] and inherits it's [`FilteredAccess`]. + pub fn from_builder(builder: &mut QueryBuilder) -> Self { + let mut fetch_state = D::init_state(builder.world_mut()); + let filter_state = F::init_state(builder.world_mut()); + D::set_access(&mut fetch_state, builder.access()); + + let mut state = Self { + world_id: builder.world().id(), + archetype_generation: ArchetypeGeneration::initial(), + matched_table_ids: Vec::new(), + matched_archetype_ids: Vec::new(), + fetch_state, + filter_state, + component_access: builder.access().clone(), + matched_tables: Default::default(), + matched_archetypes: Default::default(), + archetype_component_access: Default::default(), + #[cfg(feature = "trace")] + par_iter_span: bevy_utils::tracing::info_span!( + "par_for_each", + data = std::any::type_name::(), + filter = std::any::type_name::(), + ), + }; + state.update_archetypes(builder.world()); + state + } + /// Checks if the query is empty for the given [`World`], where the last change and current tick are given. /// /// # Panics @@ -250,17 +278,10 @@ impl QueryState { pub 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)) { - D::update_archetype_component_access( - &self.fetch_state, - archetype, - &mut self.archetype_component_access, - ); - F::update_archetype_component_access( - &self.filter_state, - archetype, - &mut self.archetype_component_access, - ); + self.update_archetype_component_access(archetype); + let archetype_index = archetype.id().index(); if !self.matched_archetypes.contains(archetype_index) { self.matched_archetypes.grow(archetype_index + 1); @@ -276,6 +297,86 @@ impl QueryState { } } + /// Returns `true` if this query matches a set of components. Otherwise, returns `false`. + pub fn matches_component_set(&self, set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { + self.component_access.filter_sets.iter().any(|set| { + set.with + .ones() + .all(|index| set_contains_id(ComponentId::get_sparse_set_index(index))) + && set + .without + .ones() + .all(|index| !set_contains_id(ComponentId::get_sparse_set_index(index))) + }) + } + + /// For the given `archetype`, adds any component accessed used by this query's underlying [`FilteredAccess`] to `access`. + pub fn update_archetype_component_access(&mut self, archetype: &Archetype) { + self.component_access.access.reads().for_each(|id| { + if let Some(id) = archetype.get_archetype_component_id(id) { + self.archetype_component_access.add_read(id); + } + }); + self.component_access.access.writes().for_each(|id| { + if let Some(id) = archetype.get_archetype_component_id(id) { + self.archetype_component_access.add_write(id); + } + }); + } + + /// 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. + /// + /// You should not call [`update_archetypes`](Self::update_archetypes) on the returned [`QueryState`] as the result will be unpredictable. + /// You might end up with a mix of archetypes that only matched the original query + archetypes that only match + /// the new [`QueryState`]. Most of the safe methods on [`QueryState`] call [`QueryState::update_archetypes`] internally, so this + /// best used through a [`Query`](crate::system::Query). + pub fn transmute(&self, world: &World) -> QueryState { + self.transmute_filtered::(world) + } + + /// Creates a new [`QueryState`] with the same underlying [`FilteredAccess`], matched tables and archetypes + /// as self but with a new type signature. + /// + /// Panics if `NewD` or `NewF` require accesses that this query does not have. + pub fn transmute_filtered( + &self, + world: &World, + ) -> QueryState { + let mut component_access = FilteredAccess::default(); + let mut fetch_state = NewD::get_state(world).expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); + let filter_state = NewF::get_state(world).expect("Could not create filter_state, Please initialize all referenced components before transmuting."); + + NewD::set_access(&mut fetch_state, &self.component_access); + NewD::update_component_access(&fetch_state, &mut component_access); + + let mut filter_component_access = FilteredAccess::default(); + NewF::update_component_access(&filter_state, &mut filter_component_access); + + component_access.extend(&filter_component_access); + assert!(component_access.is_subset(&self.component_access), "Transmuted state for {} attempts to access terms that are not allowed by original state {}.", std::any::type_name::<(NewD, NewF)>(), std::any::type_name::<(D, F)>() ); + + QueryState { + world_id: self.world_id, + archetype_generation: self.archetype_generation, + matched_table_ids: self.matched_table_ids.clone(), + matched_archetype_ids: self.matched_archetype_ids.clone(), + fetch_state, + filter_state, + component_access: self.component_access.clone(), + matched_tables: self.matched_tables.clone(), + matched_archetypes: self.matched_archetypes.clone(), + archetype_component_access: self.archetype_component_access.clone(), + #[cfg(feature = "trace")] + par_iter_span: bevy_utils::tracing::info_span!( + "par_for_each", + query = std::any::type_name::(), + filter = std::any::type_name::(), + ), + } + } + /// Gets the query result for the given [`World`] and [`Entity`]. /// /// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries. @@ -306,7 +407,7 @@ impl QueryState { /// /// # Examples /// - /// ```rust + /// ``` /// use bevy_ecs::prelude::*; /// use bevy_ecs::query::QueryEntityError; /// @@ -376,7 +477,7 @@ impl QueryState { /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is /// returned instead. /// - /// ```rust + /// ``` /// use bevy_ecs::prelude::*; /// use bevy_ecs::query::QueryEntityError; /// @@ -996,7 +1097,7 @@ impl QueryState { } /// Runs `func` on each query result for the given [`World`]. This is faster than the equivalent - /// iter() method, but cannot be chained like a normal [`Iterator`]. + /// `iter()` method, but cannot be chained like a normal [`Iterator`]. /// /// This can only be called for read-only queries, see [`Self::for_each_mut`] for write-queries. /// @@ -1024,7 +1125,7 @@ impl QueryState { } /// Runs `func` on each query result for the given [`World`]. This is faster than the equivalent - /// iter() method, but cannot be chained like a normal [`Iterator`]. + /// `iter()` method, but cannot be chained like a normal [`Iterator`]. /// /// # Safety /// @@ -1086,7 +1187,7 @@ impl QueryState { /// Runs `func` on each query result in parallel for the given [`World`], where the last change and /// the current change tick are given. This is faster than the equivalent - /// iter() method, but cannot be chained like a normal [`Iterator`]. + /// `iter()` method, but cannot be chained like a normal [`Iterator`]. /// /// # Panics /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being @@ -1305,9 +1406,17 @@ impl QueryState { } } +impl From> for QueryState { + fn from(mut value: QueryBuilder) -> Self { + QueryState::from_builder(&mut value) + } +} + #[cfg(test)] mod tests { - use crate::{prelude::*, query::QueryEntityError}; + use crate as bevy_ecs; + use crate::world::FilteredEntityRef; + use crate::{component::Component, prelude::*, query::QueryEntityError}; #[test] fn get_many_unchecked_manual_uniqueness() { @@ -1412,4 +1521,201 @@ mod tests { let mut query_state = world_1.query::(); let _panics = query_state.get_many_mut(&mut world_2, []); } + + #[derive(Component, PartialEq, Debug)] + struct A(usize); + + #[derive(Component, PartialEq, Debug)] + struct B(usize); + + #[derive(Component, PartialEq, Debug)] + struct C(usize); + + #[test] + fn can_transmute_to_more_general() { + let mut world = World::new(); + world.spawn((A(1), B(0))); + + let query_state = world.query::<(&A, &B)>(); + let mut new_query_state = query_state.transmute::<&A>(&world); + assert_eq!(new_query_state.iter(&world).len(), 1); + let a = new_query_state.single(&world); + + assert_eq!(a.0, 1); + } + + #[test] + fn cannot_get_data_not_in_original_query() { + let mut world = World::new(); + world.spawn((A(0), B(0))); + world.spawn((A(1), B(0), C(0))); + + let query_state = world.query_filtered::<(&A, &B), Without>(); + let mut new_query_state = query_state.transmute::<&A>(&world); + // even though we change the query to not have Without, we do not get the component with C. + let a = new_query_state.single(&world); + + assert_eq!(a.0, 0); + } + + #[test] + fn can_transmute_empty_tuple() { + let mut world = World::new(); + world.init_component::(); + let entity = world.spawn(A(10)).id(); + + let q = world.query::<()>(); + let mut q = q.transmute::(&world); + assert_eq!(q.single(&world), entity); + } + + #[test] + fn can_transmute_immut_fetch() { + let mut world = World::new(); + world.spawn(A(10)); + + let q = world.query::<&A>(); + let mut new_q = q.transmute::>(&world); + assert!(new_q.single(&world).is_added()); + + let q = world.query::>(); + let _ = q.transmute::<&A>(&world); + } + + #[test] + fn can_transmute_mut_fetch() { + let mut world = World::new(); + world.spawn(A(0)); + + let q = world.query::<&mut A>(); + let _ = q.transmute::>(&world); + let _ = q.transmute::<&A>(&world); + } + + #[test] + fn can_transmute_entity_mut() { + let mut world = World::new(); + world.spawn(A(0)); + + let q: QueryState> = world.query::(); + let _ = q.transmute::(&world); + } + + #[test] + fn can_generalize_with_option() { + let mut world = World::new(); + world.spawn((A(0), B(0))); + + let query_state = world.query::<(Option<&A>, &B)>(); + let _ = query_state.transmute::>(&world); + let _ = query_state.transmute::<&B>(&world); + } + + #[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, ())." + )] + fn cannot_transmute_to_include_data_not_in_original_query() { + let mut world = World::new(); + world.init_component::(); + world.init_component::(); + world.spawn(A(0)); + + let query_state = world.query::<&A>(); + let mut _new_query_state = query_state.transmute::<(&A, &B)>(&world); + } + + #[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, ())." + )] + fn cannot_transmute_immut_to_mut() { + let mut world = World::new(); + world.spawn(A(0)); + + let query_state = world.query::<&A>(); + let mut _new_query_state = query_state.transmute::<&mut A>(&world); + } + + #[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>, ())." + )] + fn cannot_transmute_option_to_immut() { + let mut world = World::new(); + world.spawn(C(0)); + + let query_state = world.query::>(); + let mut new_query_state = query_state.transmute::<&A>(&world); + let x = new_query_state.single(&world); + assert_eq!(x.0, 1234); + } + + #[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, ())." + )] + fn cannot_transmute_entity_ref() { + let mut world = World::new(); + world.init_component::(); + + let q = world.query::(); + let _ = q.transmute::<&A>(&world); + } + + #[test] + fn can_transmute_filtered_entity() { + let mut world = World::new(); + let entity = world.spawn((A(0), B(1))).id(); + let query = + QueryState::<(Entity, &A, &B)>::new(&mut world).transmute::(&world); + + let mut query = query; + // Our result is completely untyped + let entity_ref = query.single(&world); + + assert_eq!(entity, entity_ref.id()); + assert_eq!(0, entity_ref.get::().unwrap().0); + assert_eq!(1, entity_ref.get::().unwrap().0); + } + + #[test] + fn can_transmute_added() { + let mut world = World::new(); + let entity_a = world.spawn(A(0)).id(); + + let mut query = QueryState::<(Entity, &A, Has)>::new(&mut world) + .transmute_filtered::<(Entity, Has), Added>(&world); + + assert_eq!((entity_a, false), query.single(&world)); + + world.clear_trackers(); + + let entity_b = world.spawn((A(0), B(0))).id(); + assert_eq!((entity_b, true), query.single(&world)); + + world.clear_trackers(); + + assert!(query.get_single(&world).is_err()); + } + + #[test] + fn can_transmute_changed() { + let mut world = World::new(); + let entity_a = world.spawn(A(0)).id(); + + let mut detection_query = QueryState::<(Entity, &A)>::new(&mut world) + .transmute_filtered::>(&world); + + let mut change_query = QueryState::<&mut A>::new(&mut world); + assert_eq!(entity_a, detection_query.single(&world)); + + world.clear_trackers(); + + assert!(detection_query.get_single(&world).is_err()); + + change_query.single_mut(&mut world).0 = 1; + + assert_eq!(entity_a, detection_query.single(&world)); + } } diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index 758a3e56b0fe3..ad09c5fe447bf 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -1,8 +1,8 @@ use crate::{ - archetype::{Archetype, ArchetypeComponentId}, + archetype::Archetype, component::{ComponentId, Tick}, entity::Entity, - query::{Access, FilteredAccess}, + query::FilteredAccess, storage::{Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -14,13 +14,11 @@ use bevy_utils::all_tuples; /// # Safety /// /// Implementor must ensure that -/// [`update_component_access`], [`update_archetype_component_access`], [`matches_component_set`], and [`fetch`] +/// [`update_component_access`], [`matches_component_set`], and [`fetch`] /// obey the following: /// /// - For each component mutably accessed by [`fetch`], [`update_component_access`] should add write access unless read or write access has already been added, in which case it should panic. /// - For each component readonly accessed by [`fetch`], [`update_component_access`] should add read access unless write access has already been added, in which case it should panic. -/// - For each component mutably accessed by [`fetch`], [`update_archetype_component_access`] should add write access if that component belongs to the archetype. -/// - For each component readonly accessed by [`fetch`], [`update_archetype_component_access`] should add read access if that component belongs to the archetype. /// - If `fetch` mutably accesses the same component twice, [`update_component_access`] should panic. /// - [`update_component_access`] may not add a `Without` filter for a component unless [`matches_component_set`] always returns `false` when the component set contains that component. /// - [`update_component_access`] may not add a `With` filter for a component unless [`matches_component_set`] always returns `false` when the component set doesn't contain that component. @@ -34,7 +32,6 @@ use bevy_utils::all_tuples; /// [`fetch`]: Self::fetch /// [`matches_component_set`]: Self::matches_component_set /// [`Query`]: crate::system::Query -/// [`update_archetype_component_access`]: Self::update_archetype_component_access /// [`update_component_access`]: Self::update_component_access /// [`QueryData`]: crate::query::QueryData /// [`QueryFilter`]: crate::query::QueryFilter @@ -84,7 +81,6 @@ pub unsafe trait WorldQuery { /// # Safety /// /// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. - /// - [`Self::update_archetype_component_access`] must have been previously called with `archetype`. /// - `table` must correspond to `archetype`. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. unsafe fn set_archetype<'w>( @@ -100,11 +96,15 @@ pub unsafe trait WorldQuery { /// # Safety /// /// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. - /// - `table` must belong to an archetype that was previously registered with - /// [`Self::update_archetype_component_access`]. /// - `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); + /// Sets available accesses for implementors with dynamic access such as [`FilteredEntityRef`](crate::world::FilteredEntityRef) + /// or [`FilteredEntityMut`](crate::world::FilteredEntityMut). + /// + /// Called when constructing a [`QueryLens`](crate::system::QueryLens) or calling [`QueryState::from_builder`](super::QueryState::from_builder) + fn set_access(_state: &mut Self::State, _access: &FilteredAccess) {} + /// Fetch [`Self::Item`](`WorldQuery::Item`) for either the given `entity` in the current [`Table`], /// or for the given `entity` in the current [`Archetype`]. This must always be called after /// [`WorldQuery::set_table`] with a `table_row` in the range of the current [`Table`] or after @@ -125,18 +125,12 @@ pub unsafe trait WorldQuery { // and forgetting to do so would be unsound. fn update_component_access(state: &Self::State, access: &mut FilteredAccess); - /// For the given `archetype`, adds any component accessed used by this [`WorldQuery`] to `access`. - // This does not have a default body of `{}` because 99% of cases need to add accesses - // and forgetting to do so would be unsound. - fn update_archetype_component_access( - state: &Self::State, - archetype: &Archetype, - access: &mut Access, - ); - /// Creates and initializes a [`State`](WorldQuery::State) for this [`WorldQuery`] type. fn init_state(world: &mut World) -> Self::State; + /// Attempts to initializes a [`State`](WorldQuery::State) for this [`WorldQuery`] type. + fn get_state(world: &World) -> Option; + /// Returns `true` if this query matches a set of components. Otherwise, returns `false`. fn matches_component_set( state: &Self::State, @@ -210,15 +204,14 @@ macro_rules! impl_tuple_world_query { $($name::update_component_access($name, _access);)* } - fn update_archetype_component_access(state: &Self::State, _archetype: &Archetype, _access: &mut Access) { - let ($($name,)*) = state; - $($name::update_archetype_component_access($name, _archetype, _access);)* - } - fn init_state(_world: &mut World) -> Self::State { ($($name::init_state(_world),)*) } + fn get_state(_world: &World) -> Option { + Some(($($name::get_state(_world)?,)*)) + } + fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { let ($($name,)*) = state; true $(&& $name::matches_component_set($name, _set_contains_id))* diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index e5ea2de2b16d8..22905f53b8212 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -1,19 +1,17 @@ //! Definitions for [`Bundle`] reflection. +//! This allows inserting, updating and/or removing bundles whose type is only known at runtime. //! //! This module exports two types: [`ReflectBundleFns`] and [`ReflectBundle`]. //! //! Same as [`super::component`], but for bundles. use std::any::TypeId; -use crate::{ - prelude::Bundle, - world::{EntityWorldMut, FromWorld, World}, -}; -use bevy_reflect::{FromType, Reflect, ReflectRef, TypeRegistry}; +use crate::{prelude::Bundle, world::EntityWorldMut}; +use bevy_reflect::{FromReflect, FromType, Reflect, ReflectRef, TypeRegistry}; use super::ReflectComponent; -/// A struct used to operate on reflected [`Bundle`] of a type. +/// A struct used to operate on reflected [`Bundle`] trait of a type. /// /// A [`ReflectBundle`] for type `T` can be obtained via /// [`bevy_reflect::TypeRegistration::data`]. @@ -25,8 +23,6 @@ pub struct ReflectBundle(ReflectBundleFns); /// The also [`super::component::ReflectComponentFns`]. #[derive(Clone)] pub struct ReflectBundleFns { - /// Function pointer implementing [`ReflectBundle::from_world()`]. - pub from_world: fn(&mut World) -> Box, /// Function pointer implementing [`ReflectBundle::insert()`]. pub insert: fn(&mut EntityWorldMut, &dyn Reflect), /// Function pointer implementing [`ReflectBundle::apply()`]. @@ -43,17 +39,12 @@ impl ReflectBundleFns { /// /// This is useful if you want to start with the default implementation before overriding some /// of the functions to create a custom implementation. - pub fn new() -> Self { + pub fn new() -> Self { >::from_type().0 } } impl ReflectBundle { - /// Constructs default reflected [`Bundle`] from world using [`from_world()`](FromWorld::from_world). - pub fn from_world(&self, world: &mut World) -> Box { - (self.0.from_world)(world) - } - /// Insert a reflected [`Bundle`] into the entity like [`insert()`](EntityWorldMut::insert). pub fn insert(&self, entity: &mut EntityWorldMut, bundle: &dyn Reflect) { (self.0.insert)(entity, bundle); @@ -123,49 +114,53 @@ impl ReflectBundle { } } -impl FromType for ReflectBundle { +impl FromType for ReflectBundle { fn from_type() -> Self { ReflectBundle(ReflectBundleFns { - from_world: |world| Box::new(B::from_world(world)), insert: |entity, reflected_bundle| { - let mut bundle = entity.world_scope(|world| B::from_world(world)); - bundle.apply(reflected_bundle); + let bundle = B::from_reflect(reflected_bundle).unwrap(); entity.insert(bundle); }, apply: |entity, reflected_bundle, registry| { - let mut bundle = entity.world_scope(|world| B::from_world(world)); - bundle.apply(reflected_bundle); - - match bundle.reflect_ref() { - ReflectRef::Struct(bundle) => bundle - .iter_fields() - .for_each(|field| insert_field::(entity, field, registry)), - ReflectRef::Tuple(bundle) => bundle - .iter_fields() - .for_each(|field| insert_field::(entity, field, registry)), - _ => panic!( - "expected bundle `{}` to be named struct or tuple", - // FIXME: once we have unique reflect, use `TypePath`. - std::any::type_name::(), - ), + if let Some(reflect_component) = + registry.get_type_data::(TypeId::of::()) + { + reflect_component.apply(entity, reflected_bundle); + } else { + match reflected_bundle.reflect_ref() { + ReflectRef::Struct(bundle) => bundle + .iter_fields() + .for_each(|field| insert_field(entity, field, registry)), + ReflectRef::Tuple(bundle) => bundle + .iter_fields() + .for_each(|field| insert_field(entity, field, registry)), + _ => panic!( + "expected bundle `{}` to be named struct or tuple", + // FIXME: once we have unique reflect, use `TypePath`. + std::any::type_name::(), + ), + } } }, apply_or_insert: |entity, reflected_bundle, registry| { - let mut bundle = entity.world_scope(|world| B::from_world(world)); - bundle.apply(reflected_bundle); - - match bundle.reflect_ref() { - ReflectRef::Struct(bundle) => bundle - .iter_fields() - .for_each(|field| apply_or_insert_field::(entity, field, registry)), - ReflectRef::Tuple(bundle) => bundle - .iter_fields() - .for_each(|field| apply_or_insert_field::(entity, field, registry)), - _ => panic!( - "expected bundle `{}` to be named struct or tuple", - // FIXME: once we have unique reflect, use `TypePath`. - std::any::type_name::(), - ), + if let Some(reflect_component) = + registry.get_type_data::(TypeId::of::()) + { + reflect_component.apply_or_insert(entity, reflected_bundle, registry); + } else { + match reflected_bundle.reflect_ref() { + ReflectRef::Struct(bundle) => bundle + .iter_fields() + .for_each(|field| apply_or_insert_field(entity, field, registry)), + ReflectRef::Tuple(bundle) => bundle + .iter_fields() + .for_each(|field| apply_or_insert_field(entity, field, registry)), + _ => panic!( + "expected bundle `{}` to be named struct or tuple", + // FIXME: once we have unique reflect, use `TypePath`. + std::any::type_name::(), + ), + } } }, remove: |entity| { @@ -175,54 +170,58 @@ impl FromType for ReflectBundle { } } -fn insert_field( - entity: &mut EntityWorldMut, - field: &dyn Reflect, - registry: &TypeRegistry, -) { +fn insert_field(entity: &mut EntityWorldMut, field: &dyn Reflect, registry: &TypeRegistry) { if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { reflect_component.apply(entity, field); } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { reflect_bundle.apply(entity, field, registry); } else { - entity.world_scope(|world| { - if world.components().get_id(TypeId::of::()).is_some() { - panic!( - "no `ReflectComponent` registration found for `{}`", - field.reflect_type_path(), - ); - }; - }); - - panic!( - "no `ReflectBundle` registration found for `{}`", - field.reflect_type_path(), - ) + let is_component = entity + .world() + .components() + .get_id(field.type_id()) + .is_some(); + + if is_component { + panic!( + "no `ReflectComponent` registration found for `{}`", + field.reflect_type_path(), + ); + } else { + panic!( + "no `ReflectBundle` registration found for `{}`", + field.reflect_type_path(), + ) + } } } -fn apply_or_insert_field( +fn apply_or_insert_field( entity: &mut EntityWorldMut, field: &dyn Reflect, registry: &TypeRegistry, ) { if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { - reflect_component.apply_or_insert(entity, field); + reflect_component.apply_or_insert(entity, field, registry); } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { reflect_bundle.apply_or_insert(entity, field, registry); } else { - entity.world_scope(|world| { - if world.components().get_id(TypeId::of::()).is_some() { - panic!( - "no `ReflectComponent` registration found for `{}`", - field.reflect_type_path(), - ); - }; - }); - - panic!( - "no `ReflectBundle` registration found for `{}`", - field.reflect_type_path(), - ) + let is_component = entity + .world() + .components() + .get_id(field.type_id()) + .is_some(); + + if is_component { + panic!( + "no `ReflectComponent` registration found for `{}`", + field.reflect_type_path(), + ); + } else { + panic!( + "no `ReflectBundle` registration found for `{}`", + field.reflect_type_path(), + ) + } } } diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index d91e367c41b5e..2a17c8aff0634 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -1,4 +1,6 @@ //! Definitions for [`Component`] reflection. +//! This allows inserting, updating, removing and generally interacting with components +//! whose types are only known at runtime. //! //! This module exports two types: [`ReflectComponentFns`] and [`ReflectComponent`]. //! @@ -13,8 +15,17 @@ //! type, it tells the derive macro for `Reflect` to add the following single line to its //! [`get_type_registration`] method (see the relevant code[^1]). //! -//! ```ignore +//! ``` +//! # use bevy_reflect::{FromType, Reflect}; +//! # use bevy_ecs::prelude::{ReflectComponent, Component}; +//! # #[derive(Default, Reflect, Component)] +//! # struct A; +//! # impl A { +//! # fn foo() { +//! # let mut registration = bevy_reflect::TypeRegistration::of::(); //! registration.insert::(FromType::::from_type()); +//! # } +//! # } //! ``` //! //! This line adds a `ReflectComponent` to the registration data for the type in question. @@ -46,15 +57,18 @@ //! //! [`get_type_registration`]: bevy_reflect::GetTypeRegistration::get_type_registration +use std::any::TypeId; + +use super::ReflectFromWorld; use crate::{ change_detection::Mut, component::Component, entity::Entity, - world::{unsafe_world_cell::UnsafeEntityCell, EntityRef, EntityWorldMut, FromWorld, World}, + world::{unsafe_world_cell::UnsafeEntityCell, EntityRef, EntityWorldMut, World}, }; -use bevy_reflect::{FromType, Reflect}; +use bevy_reflect::{FromReflect, FromType, Reflect, TypeRegistry}; -/// A struct used to operate on reflected [`Component`] of a type. +/// A struct used to operate on reflected [`Component`] trait of a type. /// /// A [`ReflectComponent`] for type `T` can be obtained via /// [`bevy_reflect::TypeRegistration::data`]. @@ -71,7 +85,7 @@ pub struct ReflectComponent(ReflectComponentFns); /// > will not need. /// > Usually a [`ReflectComponent`] is created for a type by deriving [`Reflect`] /// > and adding the `#[reflect(Component)]` attribute. -/// > After adding the component to the [`TypeRegistry`][bevy_reflect::TypeRegistry], +/// > After adding the component to the [`TypeRegistry`], /// > its [`ReflectComponent`] can then be retrieved when needed. /// /// Creating a custom [`ReflectComponent`] may be useful if you need to create new component types @@ -83,14 +97,12 @@ pub struct ReflectComponent(ReflectComponentFns); /// world. #[derive(Clone)] pub struct ReflectComponentFns { - /// Function pointer implementing [`ReflectComponent::from_world()`]. - pub from_world: fn(&mut World) -> Box, /// Function pointer implementing [`ReflectComponent::insert()`]. - pub insert: fn(&mut EntityWorldMut, &dyn Reflect), + pub insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), /// Function pointer implementing [`ReflectComponent::apply()`]. pub apply: fn(&mut EntityWorldMut, &dyn Reflect), /// Function pointer implementing [`ReflectComponent::apply_or_insert()`]. - pub apply_or_insert: fn(&mut EntityWorldMut, &dyn Reflect), + pub apply_or_insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), /// Function pointer implementing [`ReflectComponent::remove()`]. pub remove: fn(&mut EntityWorldMut), /// Function pointer implementing [`ReflectComponent::contains()`]. @@ -105,7 +117,7 @@ pub struct ReflectComponentFns { /// The function may only be called with an [`UnsafeEntityCell`] that can be used to mutably access the relevant component on the given entity. pub reflect_unchecked_mut: unsafe fn(UnsafeEntityCell<'_>) -> Option>, /// Function pointer implementing [`ReflectComponent::copy()`]. - pub copy: fn(&World, &mut World, Entity, Entity), + pub copy: fn(&World, &mut World, Entity, Entity, &TypeRegistry), } impl ReflectComponentFns { @@ -114,20 +126,20 @@ impl ReflectComponentFns { /// /// This is useful if you want to start with the default implementation before overriding some /// of the functions to create a custom implementation. - pub fn new() -> Self { + pub fn new() -> Self { >::from_type().0 } } impl ReflectComponent { - /// Constructs default reflected [`Component`] from world using [`from_world()`](FromWorld::from_world). - pub fn from_world(&self, world: &mut World) -> Box { - (self.0.from_world)(world) - } - /// Insert a reflected [`Component`] into the entity like [`insert()`](EntityWorldMut::insert). - pub fn insert(&self, entity: &mut EntityWorldMut, component: &dyn Reflect) { - (self.0.insert)(entity, component); + pub fn insert( + &self, + entity: &mut EntityWorldMut, + component: &dyn Reflect, + registry: &TypeRegistry, + ) { + (self.0.insert)(entity, component, registry); } /// Uses reflection to set the value of this [`Component`] type in the entity to the given value. @@ -140,8 +152,13 @@ impl ReflectComponent { } /// Uses reflection to set the value of this [`Component`] type in the entity to the given value or insert a new one if it does not exist. - pub fn apply_or_insert(&self, entity: &mut EntityWorldMut, component: &dyn Reflect) { - (self.0.apply_or_insert)(entity, component); + pub fn apply_or_insert( + &self, + entity: &mut EntityWorldMut, + component: &dyn Reflect, + registry: &TypeRegistry, + ) { + (self.0.apply_or_insert)(entity, component, registry); } /// Removes this [`Component`] type from the entity. Does nothing if it doesn't exist. @@ -191,12 +208,14 @@ impl ReflectComponent { destination_world: &mut World, source_entity: Entity, destination_entity: Entity, + registry: &TypeRegistry, ) { (self.0.copy)( source_world, destination_world, source_entity, destination_entity, + registry, ); } @@ -236,25 +255,26 @@ impl ReflectComponent { } } -impl FromType for ReflectComponent { +impl FromType for ReflectComponent { fn from_type() -> Self { ReflectComponent(ReflectComponentFns { - from_world: |world| Box::new(C::from_world(world)), - insert: |entity, reflected_component| { - let mut component = entity.world_scope(|world| C::from_world(world)); - component.apply(reflected_component); + insert: |entity, reflected_component, registry| { + let component = entity.world_scope(|world| { + from_reflect_or_world::(reflected_component, world, registry) + }); entity.insert(component); }, apply: |entity, reflected_component| { let mut component = entity.get_mut::().unwrap(); component.apply(reflected_component); }, - apply_or_insert: |entity, reflected_component| { + apply_or_insert: |entity, reflected_component, registry| { if let Some(mut component) = entity.get_mut::() { component.apply(reflected_component); } else { - let mut component = entity.world_scope(|world| C::from_world(world)); - component.apply(reflected_component); + let component = entity.world_scope(|world| { + from_reflect_or_world::(reflected_component, world, registry) + }); entity.insert(component); } }, @@ -262,10 +282,10 @@ impl FromType for ReflectComponent { entity.remove::(); }, contains: |entity| entity.contains::(), - copy: |source_world, destination_world, source_entity, destination_entity| { + copy: |source_world, destination_world, source_entity, destination_entity, registry| { let source_component = source_world.get::(source_entity).unwrap(); - let mut destination_component = C::from_world(destination_world); - destination_component.apply(source_component); + let destination_component = + from_reflect_or_world::(source_component, destination_world, registry); destination_world .entity_mut(destination_entity) .insert(destination_component); @@ -290,3 +310,47 @@ impl FromType for ReflectComponent { }) } } + +/// Creates a `T` from a `&dyn Reflect`. +/// +/// The first approach uses `T`'s implementation of `FromReflect`. +/// If this fails, it falls back to default-initializing a new instance of `T` using its +/// `ReflectFromWorld` data from the `world`'s `AppTypeRegistry` and `apply`ing the +/// `&dyn Reflect` on it. +/// +/// Panics if both approaches fail. +fn from_reflect_or_world( + reflected: &dyn Reflect, + world: &mut World, + registry: &TypeRegistry, +) -> T { + if let Some(value) = T::from_reflect(reflected) { + return value; + } + + // Clone the `ReflectFromWorld` because it's cheap and "frees" + // the borrow of `world` so that it can be passed to `from_world`. + let Some(reflect_from_world) = registry.get_type_data::(TypeId::of::()) + else { + panic!( + "`FromReflect` failed and no `ReflectFromWorld` registration found for `{}`", + // FIXME: once we have unique reflect, use `TypePath`. + std::any::type_name::(), + ); + }; + + let Ok(mut value) = reflect_from_world + .from_world(world) + .into_any() + .downcast::() + else { + panic!( + "the `ReflectFromWorld` registration for `{}` produced a value of a different type", + // FIXME: once we have unique reflect, use `TypePath`. + std::any::type_name::(), + ); + }; + + value.apply(reflected); + *value +} diff --git a/crates/bevy_ecs/src/reflect/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs index d1ca60eb12001..2e0f6495581a5 100644 --- a/crates/bevy_ecs/src/reflect/entity_commands.rs +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -27,7 +27,7 @@ pub trait ReflectCommandExt { /// /// # Example /// - /// ```rust + /// ``` /// // Note that you need to register the component type in the AppTypeRegistry prior to using /// // reflection. You can use the helpers on the App with `app.register_type::()` /// // or write to the TypeRegistry directly to register all your components @@ -97,7 +97,7 @@ pub trait ReflectCommandExt { /// /// # Example /// - /// ```rust + /// ``` /// // Note that you need to register the component type in the AppTypeRegistry prior to using /// // reflection. You can use the helpers on the App with `app.register_type::()` /// // or write to the TypeRegistry directly to register all your components @@ -202,7 +202,7 @@ fn insert_reflect( let Some(reflect_component) = type_registration.data::() else { panic!("Could not get ReflectComponent data (for component type {type_path}) because it doesn't exist in this TypeRegistration."); }; - reflect_component.insert(&mut entity, &*component); + reflect_component.insert(&mut entity, &*component, type_registry); } /// A [`Command`] that adds the boxed reflect component to an entity using the data in diff --git a/crates/bevy_ecs/src/reflect/from_world.rs b/crates/bevy_ecs/src/reflect/from_world.rs new file mode 100644 index 0000000000000..8f9891406c9d5 --- /dev/null +++ b/crates/bevy_ecs/src/reflect/from_world.rs @@ -0,0 +1,86 @@ +//! Definitions for [`FromWorld`] reflection. +//! This allows creating instaces of types that are known only at runtime and +//! require an `&mut World` to be initialized. +//! +//! This module exports two types: [`ReflectFromWorldFns`] and [`ReflectFromWorld`]. +//! +//! Same as [`super::component`], but for [`FromWorld`]. + +use bevy_reflect::{FromType, Reflect}; + +use crate::world::{FromWorld, World}; + +/// A struct used to operate on the reflected [`FromWorld`] trait of a type. +/// +/// A [`ReflectFromWorld`] for type `T` can be obtained via +/// [`bevy_reflect::TypeRegistration::data`]. +#[derive(Clone)] +pub struct ReflectFromWorld(ReflectFromWorldFns); + +/// The raw function pointers needed to make up a [`ReflectFromWorld`]. +#[derive(Clone)] +pub struct ReflectFromWorldFns { + /// Function pointer implementing [`ReflectFromWorld::from_world()`]. + pub from_world: fn(&mut World) -> Box, +} + +impl ReflectFromWorldFns { + /// Get the default set of [`ReflectFromWorldFns`] for a specific type using its + /// [`FromType`] implementation. + /// + /// This is useful if you want to start with the default implementation before overriding some + /// of the functions to create a custom implementation. + pub fn new() -> Self { + >::from_type().0 + } +} + +impl ReflectFromWorld { + /// Constructs default reflected [`FromWorld`] from world using [`from_world()`](FromWorld::from_world). + pub fn from_world(&self, world: &mut World) -> Box { + (self.0.from_world)(world) + } + + /// Create a custom implementation of [`ReflectFromWorld`]. + /// + /// This is an advanced feature, + /// useful for scripting implementations, + /// that should not be used by most users + /// unless you know what you are doing. + /// + /// Usually you should derive [`Reflect`] and add the `#[reflect(FromWorld)]` bundle + /// to generate a [`ReflectFromWorld`] implementation automatically. + /// + /// See [`ReflectFromWorldFns`] for more information. + pub fn new(fns: ReflectFromWorldFns) -> Self { + Self(fns) + } + + /// The underlying function pointers implementing methods on `ReflectFromWorld`. + /// + /// This is useful when you want to keep track locally of an individual + /// function pointer. + /// + /// Calling [`TypeRegistry::get`] followed by + /// [`TypeRegistration::data::`] can be costly if done several + /// times per frame. Consider cloning [`ReflectFromWorld`] and keeping it + /// between frames, cloning a `ReflectFromWorld` is very cheap. + /// + /// If you only need a subset of the methods on `ReflectFromWorld`, + /// use `fn_pointers` to get the underlying [`ReflectFromWorldFns`] + /// and copy the subset of function pointers you care about. + /// + /// [`TypeRegistration::data::`]: bevy_reflect::TypeRegistration::data + /// [`TypeRegistry::get`]: bevy_reflect::TypeRegistry::get + pub fn fn_pointers(&self) -> &ReflectFromWorldFns { + &self.0 + } +} + +impl FromType for ReflectFromWorld { + fn from_type() -> Self { + ReflectFromWorld(ReflectFromWorldFns { + from_world: |world| Box::new(B::from_world(world)), + }) + } +} diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index 7dd481840ac86..544ad02932472 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -9,12 +9,14 @@ use bevy_reflect::{impl_reflect_value, ReflectDeserialize, ReflectSerialize, Typ mod bundle; mod component; mod entity_commands; +mod from_world; mod map_entities; mod resource; pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use entity_commands::ReflectCommandExt; +pub use from_world::{ReflectFromWorld, ReflectFromWorldFns}; pub use map_entities::ReflectMapEntities; pub use resource::{ReflectResource, ReflectResourceFns}; diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 916fc12ceaac3..4c29a5994b60e 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -107,7 +107,7 @@ pub trait Condition: sealed::Condition { /// # fn my_system() {} /// app.add_systems( /// // `resource_equals` will only get run if the resource `R` exists. - /// my_system.run_if(resource_exists::().and_then(resource_equals(R(0)))), + /// my_system.run_if(resource_exists::.and_then(resource_equals(R(0)))), /// ); /// # app.run(&mut world); /// ``` @@ -145,7 +145,7 @@ pub trait Condition: sealed::Condition { /// # fn my_system(mut c: ResMut) { c.0 = true; } /// app.add_systems( /// // Only run the system if either `A` or `B` exist. - /// my_system.run_if(resource_exists::().or_else(resource_exists::())), + /// my_system.run_if(resource_exists::.or_else(resource_exists::)), /// ); /// # /// # world.insert_resource(C(false)); @@ -194,6 +194,8 @@ mod sealed { /// A collection of [run conditions](Condition) that may be useful in any bevy app. pub mod common_conditions { + use bevy_utils::warn_once; + use super::NotSystem; use crate::{ change_detection::DetectChanges, @@ -245,7 +247,7 @@ pub mod common_conditions { } } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if the resource exists. /// /// # Example @@ -258,7 +260,7 @@ pub mod common_conditions { /// # let mut world = World::new(); /// app.add_systems( /// // `resource_exists` will only return true if the given resource exists in the world - /// my_system.run_if(resource_exists::()), + /// my_system.run_if(resource_exists::), /// ); /// /// fn my_system(mut counter: ResMut) { @@ -273,11 +275,11 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn resource_exists() -> impl FnMut(Option>) -> bool + Clone + pub fn resource_exists(res: Option>) -> bool where T: Resource, { - move |res: Option>| res.is_some() + res.is_some() } /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` @@ -365,7 +367,7 @@ pub mod common_conditions { } } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if the resource of the given type has been added since the condition was last checked. /// /// # Example @@ -379,7 +381,7 @@ pub mod common_conditions { /// app.add_systems( /// // `resource_added` will only return true if the /// // given resource was just added - /// my_system.run_if(resource_added::()), + /// my_system.run_if(resource_added::), /// ); /// /// fn my_system(mut counter: ResMut) { @@ -396,17 +398,17 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn resource_added() -> impl FnMut(Option>) -> bool + Clone + pub fn resource_added(res: Option>) -> bool where T: Resource, { - move |res: Option>| match res { + match res { Some(res) => res.is_added(), None => false, } } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if the resource of the given type has had its value changed since the condition /// was last checked. /// @@ -431,11 +433,11 @@ pub mod common_conditions { /// // `resource_changed` will only return true if the /// // given resource was just changed (or added) /// my_system.run_if( - /// resource_changed::() + /// resource_changed:: /// // By default detecting changes will also trigger if the resource was /// // just added, this won't work with my example so I will add a second /// // condition to make sure the resource wasn't just added - /// .and_then(not(resource_added::())) + /// .and_then(not(resource_added::)) /// ), /// ); /// @@ -453,14 +455,14 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 51); /// ``` - pub fn resource_changed() -> impl FnMut(Res) -> bool + Clone + pub fn resource_changed(res: Res) -> bool where T: Resource, { - move |res: Res| res.is_changed() + res.is_changed() } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if the resource of the given type has had its value changed since the condition /// was last checked. /// @@ -484,11 +486,11 @@ pub mod common_conditions { /// // `resource_exists_and_changed` will only return true if the /// // given resource exists and was just changed (or added) /// my_system.run_if( - /// resource_exists_and_changed::() + /// resource_exists_and_changed:: /// // By default detecting changes will also trigger if the resource was /// // just added, this won't work with my example so I will add a second /// // condition to make sure the resource wasn't just added - /// .and_then(not(resource_added::())) + /// .and_then(not(resource_added::)) /// ), /// ); /// @@ -510,11 +512,11 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 51); /// ``` - pub fn resource_exists_and_changed() -> impl FnMut(Option>) -> bool + Clone + pub fn resource_exists_and_changed(res: Option>) -> bool where T: Resource, { - move |res: Option>| match res { + match res { Some(res) => res.is_changed(), None => false, } @@ -550,7 +552,7 @@ pub mod common_conditions { /// // By default detecting changes will also trigger if the resource was /// // just added, this won't work with my example so I will add a second /// // condition to make sure the resource wasn't just added - /// .and_then(not(resource_added::())) + /// .and_then(not(resource_added::)) /// ), /// ); /// @@ -655,7 +657,7 @@ pub mod common_conditions { } } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if the state machine exists. /// /// # Example @@ -677,7 +679,7 @@ pub mod common_conditions { /// app.add_systems( /// // `state_exists` will only return true if the /// // given state exists - /// my_system.run_if(state_exists::()), + /// my_system.run_if(state_exists::), /// ); /// /// fn my_system(mut counter: ResMut) { @@ -694,16 +696,14 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn state_exists() -> impl FnMut(Option>>) -> bool + Clone { - move |current_state: Option>>| current_state.is_some() + pub fn state_exists(current_state: Option>>) -> bool { + current_state.is_some() } /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the state machine is currently in `state`. /// - /// # Panics - /// - /// The condition will panic if the resource does not exist. + /// Will return `false` if the state does not exist or if not in `state`. /// /// # Example /// @@ -748,10 +748,26 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 0); /// ``` - pub fn in_state(state: S) -> impl FnMut(Res>) -> bool + Clone { - move |current_state: Res>| *current_state == state + pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { + move |current_state: Option>>| match current_state { + Some(current_state) => *current_state == state, + None => { + warn_once!("No state matching the type for {} exists - did you forget to `add_state` when initializing the app?", { + let debug_state = format!("{state:?}"); + let result = debug_state + .split("::") + .next() + .unwrap_or("Unknown State Type"); + result.to_string() + }); + + false + } + } } + /// Identical to [`in_state`] - use that instead. + /// /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the state machine exists and is currently in `state`. /// @@ -804,24 +820,20 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 0); /// ``` + #[deprecated(since = "0.13.0", note = "use `in_state` instead.")] pub fn state_exists_and_equals( state: S, ) -> impl FnMut(Option>>) -> bool + Clone { - move |current_state: Option>>| match current_state { - Some(current_state) => *current_state == state, - None => false, - } + in_state(state) } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-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 /// schedules. Use this run condition if you want to detect any change, regardless of the value. /// - /// # Panics - /// - /// The condition will panic if the resource does not exist. + /// Returns false if the state does not exist or the state has not changed. /// /// # Example /// @@ -845,7 +857,7 @@ pub mod common_conditions { /// // `state_changed` will only return true if the /// // given states value has just been updated or /// // the state has just been added - /// my_system.run_if(state_changed::()), + /// my_system.run_if(state_changed::), /// ); /// /// fn my_system(mut counter: ResMut) { @@ -866,8 +878,11 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 2); /// ``` - pub fn state_changed() -> impl FnMut(Res>) -> bool + Clone { - move |current_state: Res>| current_state.is_changed() + pub fn state_changed(current_state: Option>>) -> bool { + let Some(current_state) = current_state else { + return false; + }; + current_state.is_changed() } /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` @@ -914,7 +929,7 @@ pub mod common_conditions { move |mut reader: EventReader| reader.read().count() > 0 } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if there are any entities with the given component type. /// /// # Example @@ -927,7 +942,7 @@ pub mod common_conditions { /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( - /// my_system.run_if(any_with_component::()), + /// my_system.run_if(any_with_component::), /// ); /// /// #[derive(Component)] @@ -947,8 +962,8 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn any_with_component() -> impl FnMut(Query<(), With>) -> bool + Clone { - move |query: Query<(), With>| !query.is_empty() + pub fn any_with_component(query: Query<(), With>) -> bool { + !query.is_empty() } /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` @@ -1199,17 +1214,17 @@ mod tests { Schedule::default().add_systems( (test_system, test_system) .distributive_run_if(run_once()) - .distributive_run_if(resource_exists::>()) - .distributive_run_if(resource_added::>()) - .distributive_run_if(resource_changed::>()) - .distributive_run_if(resource_exists_and_changed::>()) + .distributive_run_if(resource_exists::>) + .distributive_run_if(resource_added::>) + .distributive_run_if(resource_changed::>) + .distributive_run_if(resource_exists_and_changed::>) .distributive_run_if(resource_changed_or_removed::>()) .distributive_run_if(resource_removed::>()) - .distributive_run_if(state_exists::()) + .distributive_run_if(state_exists::) .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) - .distributive_run_if(state_changed::()) + .distributive_run_if(state_changed::) .distributive_run_if(on_event::()) - .distributive_run_if(any_with_component::()) + .distributive_run_if(any_with_component::) .distributive_run_if(not(run_once())), ); } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 31a4c6f9cdab4..02d69d3cb0315 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -25,7 +25,7 @@ use crate::{ world::World, }; -/// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s. +/// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s excluding the current running [`Schedule`]. #[derive(Default, Resource)] pub struct Schedules { inner: HashMap, @@ -244,6 +244,37 @@ impl Schedule { self } + /// Suppress warnings and errors that would result from systems in these sets having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + #[track_caller] + pub fn ignore_ambiguity(&mut self, a: S1, b: S2) -> &mut Self + where + S1: IntoSystemSet, + S2: IntoSystemSet, + { + let a = a.into_system_set(); + let b = b.into_system_set(); + + let Some(&a_id) = self.graph.system_set_ids.get(&a.intern()) else { + panic!( + "Could not mark system as ambiguous, `{:?}` was not found in the schedule. + Did you try to call `ambiguous_with` before adding the system to the world?", + a + ); + }; + let Some(&b_id) = self.graph.system_set_ids.get(&b.intern()) else { + panic!( + "Could not mark system as ambiguous, `{:?}` was not found in the schedule. + Did you try to call `ambiguous_with` before adding the system to the world?", + b + ); + }; + + self.graph.ambiguous_with.add_edge(a_id, b_id, ()); + + self + } + /// Configures a collection of system sets in this schedule, adding them if they does not exist. #[track_caller] pub fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) -> &mut Self { diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs index a39f8cc036d1d..6308c7731e009 100644 --- a/crates/bevy_ecs/src/schedule/state.rs +++ b/crates/bevy_ecs/src/schedule/state.rs @@ -5,6 +5,8 @@ use std::ops::Deref; use crate as bevy_ecs; use crate::change_detection::DetectChangesMut; +use crate::event::Event; +use crate::prelude::FromWorld; #[cfg(feature = "bevy_reflect")] use crate::reflect::ReflectResource; use crate::schedule::ScheduleLabel; @@ -23,12 +25,12 @@ pub use bevy_ecs_macros::States; /// You can access the current state of type `T` with the [`State`] resource, /// and the queued state with the [`NextState`] resource. /// -/// State transitions typically occur in the [`OnEnter`] and [`OnExit`] schedules, +/// State transitions typically occur in the [`OnEnter`] and [`OnExit`] schedules, /// which can be run via the [`apply_state_transition::`] system. /// /// # Example /// -/// ```rust +/// ``` /// use bevy_ecs::prelude::States; /// /// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] @@ -40,7 +42,7 @@ pub use bevy_ecs_macros::States; /// } /// /// ``` -pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug + Default {} +pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug {} /// The label of a [`Schedule`](super::Schedule) that runs whenever [`State`] /// enters this state. @@ -72,12 +74,29 @@ pub struct OnTransition { /// [`apply_state_transition::`] system. /// /// The starting state is defined via the [`Default`] implementation for `S`. -#[derive(Resource, Default, Debug)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource, Default) -)] +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn game_logic(game_state: Res>) { +/// match game_state.get() { +/// GameState::InGame => { +/// // Run game logic here... +/// }, +/// _ => {}, +/// } +/// } +/// ``` +#[derive(Resource, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] pub struct State(S); impl State { @@ -94,6 +113,12 @@ impl State { } } +impl FromWorld for State { + fn from_world(world: &mut World) -> Self { + Self(S::from_world(world)) + } +} + impl PartialEq for State { fn eq(&self, other: &S) -> bool { self.get() == other @@ -113,7 +138,23 @@ impl Deref for State { /// To queue a transition, just set the contained value to `Some(next_state)`. /// Note that these transitions can be overridden by other systems: /// only the actual value of this resource at the time of [`apply_state_transition`] matters. -#[derive(Resource, Default, Debug)] +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn start_game(mut next_game_state: ResMut>) { +/// next_game_state.set(GameState::InGame); +/// } +/// ``` +#[derive(Resource, Debug)] #[cfg_attr( feature = "bevy_reflect", derive(bevy_reflect::Reflect), @@ -121,6 +162,12 @@ impl Deref for State { )] pub struct NextState(pub Option); +impl Default for NextState { + fn default() -> Self { + Self(None) + } +} + impl NextState { /// Tentatively set a planned state transition to `Some(state)`. pub fn set(&mut self, state: S) { @@ -128,37 +175,62 @@ impl NextState { } } +/// Event sent when any state transition of `S` happens. +/// +/// 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)] +pub struct StateTransitionEvent { + /// the state we were in before + pub before: S, + /// the state we're in now + pub after: S, +} + /// Run the enter schedule (if it exists) for the current state. pub fn run_enter_schedule(world: &mut World) { - world - .try_run_schedule(OnEnter(world.resource::>().0.clone())) - .ok(); + let Some(state) = world.get_resource::>() else { + return; + }; + world.try_run_schedule(OnEnter(state.0.clone())).ok(); } /// If a new state is queued in [`NextState`], this system: /// - Takes the new state value from [`NextState`] and updates [`State`]. +/// - Sends a relevant [`StateTransitionEvent`] /// - Runs the [`OnExit(exited_state)`] schedule, if it exists. /// - Runs the [`OnTransition { from: exited_state, to: entered_state }`](OnTransition), if it exists. /// - Runs the [`OnEnter(entered_state)`] schedule, if it exists. pub fn apply_state_transition(world: &mut World) { // We want to take the `NextState` resource, // but only mark it as changed if it wasn't empty. - let mut next_state_resource = world.resource_mut::>(); + let Some(mut next_state_resource) = world.get_resource_mut::>() else { + return; + }; if let Some(entered) = next_state_resource.bypass_change_detection().0.take() { next_state_resource.set_changed(); - - let mut state_resource = world.resource_mut::>(); - if *state_resource != entered { - let exited = mem::replace(&mut state_resource.0, entered.clone()); - // Try to run the schedules if they exist. - world.try_run_schedule(OnExit(exited.clone())).ok(); - world - .try_run_schedule(OnTransition { - from: exited, - to: entered.clone(), - }) - .ok(); - world.try_run_schedule(OnEnter(entered)).ok(); - } + match world.get_resource_mut::>() { + Some(mut state_resource) => { + if *state_resource != entered { + let exited = mem::replace(&mut state_resource.0, entered.clone()); + world.send_event(StateTransitionEvent { + before: exited.clone(), + after: entered.clone(), + }); + // Try to run the schedules if they exist. + world.try_run_schedule(OnExit(exited.clone())).ok(); + world + .try_run_schedule(OnTransition { + from: exited, + to: entered.clone(), + }) + .ok(); + world.try_run_schedule(OnEnter(entered)).ok(); + } + } + None => { + world.insert_resource(State(entered.clone())); + world.try_run_schedule(OnEnter(entered)).ok(); + } + }; } } diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index 45a47b671a370..e8cf7c074b689 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -61,6 +61,8 @@ impl BlobVec { if item_layout.size() == 0 { BlobVec { data, + // ZST `BlobVec` max size is `usize::MAX`, and `reserve_exact` for ZST assumes + // the capacity is always `usize::MAX` and panics if it overflows. capacity: usize::MAX, len: 0, item_layout, @@ -109,22 +111,31 @@ impl BlobVec { /// /// Note that the allocator may give the collection more space than it requests. Therefore, capacity can not be relied upon /// to be precisely minimal. + /// + /// # Panics + /// + /// Panics if new capacity overflows `usize`. pub fn reserve_exact(&mut self, additional: usize) { let available_space = self.capacity - self.len; - if available_space < additional && self.item_layout.size() > 0 { + if available_space < additional { // SAFETY: `available_space < additional`, so `additional - available_space > 0` let increment = unsafe { NonZeroUsize::new_unchecked(additional - available_space) }; - // SAFETY: not called for ZSTs - unsafe { self.grow_exact(increment) }; + self.grow_exact(increment); } } - // SAFETY: must not be called for a ZST item layout - #[warn(unsafe_op_in_unsafe_fn)] // to allow unsafe blocks in unsafe fn - unsafe fn grow_exact(&mut self, increment: NonZeroUsize) { - debug_assert!(self.item_layout.size() != 0); - - let new_capacity = self.capacity + increment.get(); + /// Grows the capacity by `increment` elements. + /// + /// # Panics + /// + /// Panics if the new capacity overflows `usize`. + /// For ZST it panics unconditionally because ZST `BlobVec` capacity + /// is initialized to `usize::MAX` and always stays that way. + fn grow_exact(&mut self, increment: NonZeroUsize) { + let new_capacity = self + .capacity + .checked_add(increment.get()) + .expect("capacity overflow"); let new_layout = array_layout(&self.item_layout, new_capacity).expect("array layout should be valid"); let new_data = if self.capacity == 0 { @@ -306,7 +317,7 @@ impl BlobVec { /// The removed element is replaced by the last element of the `BlobVec`. /// /// # Safety - /// It is the caller's responsibility to ensure that `index` is < self.len() + /// It is the caller's responsibility to ensure that `index` is `< self.len()`. #[inline] pub unsafe fn swap_remove_and_drop_unchecked(&mut self, index: usize) { debug_assert!(index < self.len()); @@ -609,6 +620,48 @@ mod tests { let _ = unsafe { BlobVec::new(item_layout, Some(drop), 0) }; } + #[test] + #[should_panic(expected = "capacity overflow")] + fn blob_vec_zst_size_overflow() { + // SAFETY: no drop is correct drop for `()`. + let mut blob_vec = unsafe { BlobVec::new(Layout::new::<()>(), None, 0) }; + + assert_eq!(usize::MAX, blob_vec.capacity(), "Self-check"); + + // SAFETY: Because `()` is a ZST trivial drop type, and because `BlobVec` capacity + // is always `usize::MAX` for ZSTs, we can arbitrarily set the length + // and still be sound. + unsafe { + blob_vec.set_len(usize::MAX); + } + + // SAFETY: `BlobVec` was initialized for `()`, so it is safe to push `()` to it. + unsafe { + OwningPtr::make((), |ptr| { + // This should panic because len is usize::MAX, remaining capacity is 0. + blob_vec.push(ptr); + }); + } + } + + #[test] + #[should_panic(expected = "capacity overflow")] + fn blob_vec_capacity_overflow() { + // SAFETY: no drop is correct drop for `u32`. + let mut blob_vec = unsafe { BlobVec::new(Layout::new::(), None, 0) }; + + assert_eq!(0, blob_vec.capacity(), "Self-check"); + + OwningPtr::make(17u32, |ptr| { + // SAFETY: we push the value of correct type. + unsafe { + blob_vec.push(ptr); + } + }); + + blob_vec.reserve_exact(usize::MAX); + } + #[test] fn aligned_zst() { // NOTE: This test is explicitly for uncovering potential UB with miri. diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index c130e3e232b62..5eeebb1f1d04c 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,9 +1,9 @@ use crate::archetype::ArchetypeComponentId; use crate::change_detection::{MutUntyped, TicksMut}; use crate::component::{ComponentId, ComponentTicks, Components, Tick, TickCells}; -use crate::storage::{Column, SparseSet, TableRow}; +use crate::storage::{blob_vec::BlobVec, SparseSet}; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; -use std::{mem::ManuallyDrop, thread::ThreadId}; +use std::{cell::UnsafeCell, mem::ManuallyDrop, thread::ThreadId}; /// The type-erased backing storage and metadata for a single resource within a [`World`]. /// @@ -11,7 +11,9 @@ use std::{mem::ManuallyDrop, thread::ThreadId}; /// /// [`World`]: crate::world::World pub struct ResourceData { - column: ManuallyDrop, + data: ManuallyDrop, + added_ticks: UnsafeCell, + changed_ticks: UnsafeCell, type_name: String, id: ArchetypeComponentId, origin_thread_id: Option, @@ -33,14 +35,14 @@ impl Drop for ResourceData { // been dropped. The validate_access call above will check that the // data is dropped on the thread it was inserted from. unsafe { - ManuallyDrop::drop(&mut self.column); + ManuallyDrop::drop(&mut self.data); } } } impl ResourceData { - /// The only row in the underlying column. - const ROW: TableRow = TableRow::from_u32(0); + /// The only row in the underlying `BlobVec`. + const ROW: usize = 0; /// Validates the access to `!Send` resources is only done on the thread they were created from. /// @@ -65,7 +67,7 @@ impl ResourceData { /// Returns true if the resource is populated. #[inline] pub fn is_present(&self) -> bool { - !self.column.is_empty() + !self.data.is_empty() } /// Gets the [`ArchetypeComponentId`] for the resource. @@ -81,16 +83,24 @@ impl ResourceData { /// original thread it was inserted from. #[inline] pub fn get_data(&self) -> Option> { - self.column.get_data(Self::ROW).map(|res| { + self.is_present().then(|| { self.validate_access(); - res + // SAFETY: We've already checked if a value is present, and there should only be one. + unsafe { self.data.get_unchecked(Self::ROW) } }) } /// Returns a reference to the resource's change ticks, if it exists. #[inline] pub fn get_ticks(&self) -> Option { - self.column.get_ticks(Self::ROW) + // SAFETY: This is being fetched through a read-only reference to Self, so no other mutable references + // to the ticks can exist. + unsafe { + self.is_present().then(|| ComponentTicks { + added: self.added_ticks.read(), + changed: self.changed_ticks.read(), + }) + } } /// Returns references to the resource and its change ticks, if it exists. @@ -100,9 +110,16 @@ impl ResourceData { /// original thread it was inserted in. #[inline] pub(crate) fn get_with_ticks(&self) -> Option<(Ptr<'_>, TickCells<'_>)> { - self.column.get(Self::ROW).map(|res| { + self.is_present().then(|| { self.validate_access(); - res + ( + // SAFETY: We've already checked if a value is present, and there should only be one. + unsafe { self.data.get_unchecked(Self::ROW) }, + TickCells { + added: &self.added_ticks, + changed: &self.changed_ticks, + }, + ) }) } @@ -134,13 +151,18 @@ impl ResourceData { pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: Tick) { if self.is_present() { self.validate_access(); - self.column.replace(Self::ROW, value, change_tick); + // SAFETY: The caller ensures that the provided value is valid for the underlying type and + // is properly initialized. We've ensured that a value is already present and previously + // initialized. + self.data.replace_unchecked(Self::ROW, value); } else { if !SEND { self.origin_thread_id = Some(std::thread::current().id()); } - self.column.push(value, ComponentTicks::new(change_tick)); + self.data.push(value); + *self.added_ticks.deref_mut() = change_tick; } + *self.changed_ticks.deref_mut() = change_tick; } /// Inserts a value into the resource with a pre-existing change tick. If a @@ -160,18 +182,18 @@ impl ResourceData { ) { if self.is_present() { self.validate_access(); - self.column.replace_untracked(Self::ROW, value); - *self.column.get_added_tick_unchecked(Self::ROW).deref_mut() = change_ticks.added; - *self - .column - .get_changed_tick_unchecked(Self::ROW) - .deref_mut() = change_ticks.changed; + // SAFETY: The caller ensures that the provided value is valid for the underlying type and + // is properly initialized. We've ensured that a value is already present and previously + // initialized. + self.data.replace_unchecked(Self::ROW, value); } else { if !SEND { self.origin_thread_id = Some(std::thread::current().id()); } - self.column.push(value, change_ticks); + self.data.push(value); } + *self.added_ticks.deref_mut() = change_ticks.added; + *self.changed_ticks.deref_mut() = change_ticks.changed; } /// Removes a value from the resource, if present. @@ -182,12 +204,24 @@ impl ResourceData { #[inline] #[must_use = "The returned pointer to the removed component should be used or dropped"] pub(crate) fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> { - if SEND { - self.column.swap_remove_and_forget(Self::ROW) - } else { - self.is_present() - .then(|| self.validate_access()) - .and_then(|_| self.column.swap_remove_and_forget(Self::ROW)) + if !self.is_present() { + return None; + } + if !SEND { + self.validate_access(); + } + // SAFETY: We've already validated that the row is present. + let res = unsafe { self.data.swap_remove_and_forget_unchecked(Self::ROW) }; + // SAFETY: This function is being called through an exclusive mutable reference to Self, which + // makes it sound to read these ticks. + unsafe { + Some(( + res, + ComponentTicks { + added: self.added_ticks.read(), + changed: self.changed_ticks.read(), + }, + )) } } @@ -200,9 +234,14 @@ impl ResourceData { pub(crate) fn remove_and_drop(&mut self) { if self.is_present() { self.validate_access(); - self.column.clear(); + self.data.clear(); } } + + 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); + } } /// The backing store for all [`Resource`]s stored in the [`World`]. @@ -275,8 +314,18 @@ impl Resources { component_info.name(), ); } + // SAFETY: component_info.drop() is valid for the types that will be inserted. + let data = unsafe { + BlobVec::new( + component_info.layout(), + component_info.drop(), + 1 + ) + }; ResourceData { - column: ManuallyDrop::new(Column::with_capacity(component_info, 1)), + 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(), origin_thread_id: None, @@ -286,7 +335,7 @@ impl Resources { pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for info in self.resources.values_mut() { - info.column.check_change_ticks(change_tick); + info.check_change_ticks(change_tick); } } } diff --git a/crates/bevy_ecs/src/storage/table.rs b/crates/bevy_ecs/src/storage/table.rs index f81e6d5f8f7bc..5b05e43c29d2b 100644 --- a/crates/bevy_ecs/src/storage/table.rs +++ b/crates/bevy_ecs/src/storage/table.rs @@ -204,18 +204,6 @@ impl Column { .get_mut() = change_tick; } - /// Writes component data to the column at given row. - /// Assumes the slot is initialized, calls drop. - /// Does not update the Component's ticks. - /// - /// # Safety - /// Assumes data has already been allocated for the given row. - #[inline] - pub(crate) unsafe fn replace_untracked(&mut self, row: TableRow, data: OwningPtr<'_>) { - debug_assert!(row.as_usize() < self.len()); - self.data.replace_unchecked(row.as_usize(), data); - } - /// Gets the current number of elements stored in the column. #[inline] pub fn len(&self) -> usize { @@ -246,33 +234,7 @@ impl Column { } /// Removes an element from the [`Column`] and returns it and its change detection ticks. - /// This does not preserve ordering, but is O(1). - /// - /// The element is replaced with the last element in the [`Column`]. - /// - /// It is the caller's responsibility to ensure that the removed value is dropped or used. - /// Failure to do so may result in resources not being released (i.e. files handles not being - /// released, memory leaks, etc.) - /// - /// Returns `None` if `row` is out of bounds. - #[inline] - #[must_use = "The returned pointer should be used to drop the removed component"] - pub(crate) fn swap_remove_and_forget( - &mut self, - row: TableRow, - ) -> Option<(OwningPtr<'_>, ComponentTicks)> { - (row.as_usize() < self.data.len()).then(|| { - // SAFETY: The row was length checked before this. - let data = unsafe { 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(); - (data, ComponentTicks { added, changed }) - }) - } - - /// Removes an element from the [`Column`] and returns it and its change detection ticks. - /// This does not preserve ordering, but is O(1). Unlike [`Column::swap_remove_and_forget`] - /// this does not do any bounds checking. + /// This does not preserve ordering, but is O(1) and does not do any bounds checking. /// /// The element is replaced with the last element in the [`Column`]. /// diff --git a/crates/bevy_ecs/src/system/commands/command_queue.rs b/crates/bevy_ecs/src/system/commands/command_queue.rs index f68031c3d39b0..5130089cc8fd2 100644 --- a/crates/bevy_ecs/src/system/commands/command_queue.rs +++ b/crates/bevy_ecs/src/system/commands/command_queue.rs @@ -1,6 +1,7 @@ use std::mem::MaybeUninit; use bevy_ptr::{OwningPtr, Unaligned}; +use bevy_utils::tracing::warn; use super::Command; use crate::world::World; @@ -161,6 +162,9 @@ impl CommandQueue { impl Drop for CommandQueue { fn drop(&mut self) { + if !self.bytes.is_empty() { + warn!("CommandQueue has un-applied commands being dropped."); + } self.apply_or_drop_queued(None); } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 80de73f9f15ab..cd62763fa6376 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -409,7 +409,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, I::Item: Bundle, { - self.queue.push(SpawnBatch { bundles_iter }); + self.queue.push(spawn_batch(bundles_iter)); } /// Pushes a [`Command`] to the queue for creating entities, if needed, @@ -433,13 +433,12 @@ impl<'w, 's> Commands<'w, 's> { /// Spawning a specific `entity` value is rarely the right choice. Most apps should use [`Commands::spawn_batch`]. /// This method should generally only be used for sharing entities across apps, and only when they have a scheme /// worked out to share an ID space (which doesn't happen by default). - pub fn insert_or_spawn_batch(&mut self, bundles_iter: I) + pub fn insert_or_spawn_batch(&mut self, bundles: I) where - I: IntoIterator + Send + Sync + 'static, - I::IntoIter: Iterator, + I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue.push(InsertOrSpawnBatch { bundles_iter }); + self.queue.push(insert_or_spawn_batch(bundles)); } /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with an inferred value. @@ -467,7 +466,7 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(initialise_scoreboard); /// ``` pub fn init_resource(&mut self) { - self.queue.push(InitResource::::new()); + self.queue.push(init_resource::); } /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with a specific value. @@ -496,7 +495,7 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(system); /// ``` pub fn insert_resource(&mut self, resource: R) { - self.queue.push(InsertResource { resource }); + self.queue.push(insert_resource(resource)); } /// Pushes a [`Command`] to the queue for removing a [`Resource`] from the [`World`]. @@ -520,7 +519,7 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(system); /// ``` pub fn remove_resource(&mut self) { - self.queue.push(RemoveResource::::new()); + self.queue.push(remove_resource::); } /// Runs the system corresponding to the given [`SystemId`]. @@ -608,18 +607,14 @@ impl<'w, 's> Commands<'w, 's> { /// struct Counter(i64); /// /// /// A `Command` which names an entity based on a global counter. -/// struct CountName; -/// -/// impl EntityCommand for CountName { -/// fn apply(self, id: Entity, world: &mut World) { -/// // Get the current value of the counter, and increment it for next time. -/// let mut counter = world.resource_mut::(); -/// let i = counter.0; -/// counter.0 += 1; +/// fn count_name(entity: Entity, world: &mut World) { +/// // Get the current value of the counter, and increment it for next time. +/// let mut counter = world.resource_mut::(); +/// let i = counter.0; +/// counter.0 += 1; /// -/// // Name the entity after the value of the counter. -/// world.entity_mut(id).insert(Name::new(format!("Entity #{i}"))); -/// } +/// // Name the entity after the value of the counter. +/// world.entity_mut(entity).insert(Name::new(format!("Entity #{i}"))); /// } /// /// // App creation boilerplate omitted... @@ -635,8 +630,8 @@ impl<'w, 's> Commands<'w, 's> { /// # assert_schedule.run(&mut world); /// /// fn setup(mut commands: Commands) { -/// commands.spawn_empty().add(CountName); -/// commands.spawn_empty().add(CountName); +/// commands.spawn_empty().add(count_name); +/// commands.spawn_empty().add(count_name); /// } /// /// fn assert_names(named: Query<&Name>) { @@ -645,25 +640,33 @@ impl<'w, 's> Commands<'w, 's> { /// assert_eq!(names, HashSet::from_iter(["Entity #0", "Entity #1"])); /// } /// ``` -pub trait EntityCommand: Send + 'static { +pub trait EntityCommand: Send + 'static { /// Executes this command for the given [`Entity`]. fn apply(self, id: Entity, world: &mut World); /// Returns a [`Command`] which executes this [`EntityCommand`] for the given [`Entity`]. - fn with_entity(self, id: Entity) -> WithEntity + fn with_entity(self, id: Entity) -> WithEntity where Self: Sized, { - WithEntity { cmd: self, id } + WithEntity { + cmd: self, + id, + marker: PhantomData, + } } } /// Turns an [`EntityCommand`] type into a [`Command`] type. -pub struct WithEntity { +pub struct WithEntity> { cmd: C, id: Entity, + marker: PhantomData Marker>, } -impl Command for WithEntity { +impl> Command for WithEntity +where + M: 'static, +{ #[inline] fn apply(self, world: &mut World) { self.cmd.apply(self.id, world); @@ -747,11 +750,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.commands.add(Insert { - entity: self.entity, - bundle, - }); - self + self.add(insert(bundle)) } /// Tries to add a [`Bundle`] of components to the entity. @@ -803,11 +802,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.commands.add(TryInsert { - entity: self.entity, - bundle, - }); - self + self.add(try_insert(bundle)) } /// Removes a [`Bundle`] of components from the entity. @@ -849,8 +844,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { where T: Bundle, { - self.commands.add(Remove::::new(self.entity)); - self + self.add(remove::) } /// Despawns the entity. @@ -884,9 +878,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// # bevy_ecs::system::assert_is_system(remove_character_system); /// ``` pub fn despawn(&mut self) { - self.commands.add(Despawn { - entity: self.entity, - }); + self.add(despawn); } /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. @@ -905,7 +897,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// # } /// # bevy_ecs::system::assert_is_system(my_system); /// ``` - pub fn add(&mut self, command: C) -> &mut Self { + pub fn add(&mut self, command: impl EntityCommand) -> &mut Self { self.commands.add(command.with_entity(self.entity)); self } @@ -951,8 +943,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { where T: Bundle, { - self.commands.add(Retain::::new(self.entity)); - self + self.add(retain::) } /// Logs the components of the entity at the info level. @@ -961,9 +952,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// /// The command will panic when applied if the associated entity does not exist. pub fn log_components(&mut self) { - self.commands.add(LogComponents { - entity: self.entity, - }); + self.add(log_components); } /// Returns the underlying [`Commands`]. @@ -981,7 +970,7 @@ where } } -impl EntityCommand for F +impl EntityCommand for F where F: FnOnce(EntityWorldMut) + Send + 'static, { @@ -990,41 +979,25 @@ where } } -/// A [`Command`] that spawns a new entity and adds the components in a [`Bundle`] to it. -#[derive(Debug)] -pub struct Spawn { - /// The [`Bundle`] of components that will be added to the newly-spawned entity. - pub bundle: T, -} - -impl Command for Spawn +impl EntityCommand for F where - T: Bundle, + F: FnOnce(Entity, &mut World) + Send + 'static, { - fn apply(self, world: &mut World) { - world.spawn(self.bundle); + fn apply(self, id: Entity, world: &mut World) { + self(id, world); } } /// A [`Command`] that consumes an iterator of [`Bundle`]s to spawn a series of entities. /// /// This is more efficient than spawning the entities individually. -pub struct SpawnBatch -where - I: IntoIterator, - I::Item: Bundle, -{ - /// The iterator that returns the [`Bundle`]s which will be added to each newly-spawned entity. - pub bundles_iter: I, -} - -impl Command for SpawnBatch +fn spawn_batch(bundles: I) -> impl Command where - I: IntoIterator + Send + Sync + 'static, - I::Item: Bundle, + I: IntoIterator + Send + Sync + 'static, + B: Bundle, { - fn apply(self, world: &mut World) { - world.spawn_batch(self.bundles_iter); + move |world: &mut World| { + world.spawn_batch(bundles); } } @@ -1032,24 +1005,13 @@ where /// If any entities do not already exist in the world, they will be spawned. /// /// This is more efficient than inserting the bundles individually. -pub struct InsertOrSpawnBatch -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, - I::IntoIter: Iterator, -{ - /// The iterator that returns each [entity ID](Entity) and corresponding [`Bundle`]. - pub bundles_iter: I, -} - -impl Command for InsertOrSpawnBatch +fn insert_or_spawn_batch(bundles: I) -> impl Command where - I: IntoIterator + Send + Sync + 'static, + I: IntoIterator + Send + Sync + 'static, B: Bundle, - I::IntoIter: Iterator, { - fn apply(self, world: &mut World) { - if let Err(invalid_entities) = world.insert_or_spawn_batch(self.bundles_iter) { + move |world: &mut World| { + if let Err(invalid_entities) = world.insert_or_spawn_batch(bundles) { error!( "Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", std::any::type_name::(), @@ -1066,54 +1028,26 @@ where /// /// This won't clean up external references to the entity (such as parent-child relationships /// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. -#[derive(Debug)] -pub struct Despawn { - /// The entity that will be despawned. - pub entity: Entity, -} - -impl Command for Despawn { - fn apply(self, world: &mut World) { - world.despawn(self.entity); - } -} - -/// A [`Command`] that adds the components in a [`Bundle`] to an entity. -pub struct Insert { - /// The entity to which the components will be added. - pub entity: Entity, - /// The [`Bundle`] containing the components that will be added to the entity. - pub bundle: T, +fn despawn(entity: Entity, world: &mut World) { + world.despawn(entity); } -impl Command for Insert -where - T: Bundle + 'static, -{ - fn apply(self, world: &mut World) { - if let Some(mut entity) = world.get_entity_mut(self.entity) { - entity.insert(self.bundle); +/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity. +fn insert(bundle: T) -> impl EntityCommand { + move |entity: Entity, world: &mut World| { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.insert(bundle); } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World.", std::any::type_name::(), self.entity); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World.", std::any::type_name::(), entity); } } } -/// A [`Command`] that attempts to add the components in a [`Bundle`] to an entity. -pub struct TryInsert { - /// The entity to which the components will be added. - pub entity: Entity, - /// The [`Bundle`] containing the components that will be added to the entity. - pub bundle: T, -} - -impl Command for TryInsert -where - T: Bundle + 'static, -{ - fn apply(self, world: &mut World) { - if let Some(mut entity) = world.get_entity_mut(self.entity) { - entity.insert(self.bundle); +/// An [`EntityCommand`] that attempts to add the components in a [`Bundle`] to an entity. +fn try_insert(bundle: impl Bundle) -> impl EntityCommand { + move |entity, world: &mut World| { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.insert(bundle); } } } @@ -1121,132 +1055,47 @@ where /// A [`Command`] that removes components from an entity. /// For a [`Bundle`] type `T`, this will remove any components in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. -#[derive(Debug)] -pub struct Remove { - /// The entity from which the components will be removed. - pub entity: Entity, - _marker: PhantomData, -} - -impl Command for Remove -where - T: Bundle, -{ - fn apply(self, world: &mut World) { - if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { - entity_mut.remove::(); - } - } -} - -impl Remove { - /// Creates a [`Command`] which will remove the specified [`Entity`] when applied. - pub const fn new(entity: Entity) -> Self { - Self { - entity, - _marker: PhantomData, - } +fn remove(entity: Entity, world: &mut World) { + if let Some(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.remove::(); } } /// A [`Command`] that removes components from an entity. /// For a [`Bundle`] type `T`, this will remove all components except those in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. -#[derive(Debug)] -pub struct Retain { - /// The entity from which the components will be removed. - pub entity: Entity, - _marker: PhantomData, -} - -impl Command for Retain -where - T: Bundle, -{ - fn apply(self, world: &mut World) { - if let Some(mut entity_mut) = world.get_entity_mut(self.entity) { - entity_mut.retain::(); - } - } -} - -impl Retain { - /// Creates a [`Command`] which will remove all but the specified components when applied. - pub const fn new(entity: Entity) -> Self { - Self { - entity, - _marker: PhantomData, - } +fn retain(entity: Entity, world: &mut World) { + if let Some(mut entity_mut) = world.get_entity_mut(entity) { + entity_mut.retain::(); } } /// A [`Command`] that inserts a [`Resource`] into the world using a value /// created with the [`FromWorld`] trait. -pub struct InitResource { - _marker: PhantomData, -} - -impl Command for InitResource { - fn apply(self, world: &mut World) { - world.init_resource::(); - } -} - -impl InitResource { - /// Creates a [`Command`] which will insert a default created [`Resource`] into the [`World`] - pub const fn new() -> Self { - Self { - _marker: PhantomData, - } - } -} - -/// A [`Command`] that inserts a [`Resource`] into the world. -pub struct InsertResource { - /// The resource that will be added to the world. - pub resource: R, -} - -impl Command for InsertResource { - fn apply(self, world: &mut World) { - world.insert_resource(self.resource); - } +fn init_resource(world: &mut World) { + world.init_resource::(); } /// A [`Command`] that removes the [resource](Resource) `R` from the world. -pub struct RemoveResource { - _marker: PhantomData, -} - -impl Command for RemoveResource { - fn apply(self, world: &mut World) { - world.remove_resource::(); - } +fn remove_resource(world: &mut World) { + world.remove_resource::(); } -impl RemoveResource { - /// Creates a [`Command`] which will remove a [`Resource`] from the [`World`] - pub const fn new() -> Self { - Self { - _marker: PhantomData, - } +/// A [`Command`] that inserts a [`Resource`] into the world. +fn insert_resource(resource: R) -> impl Command { + move |world: &mut World| { + world.insert_resource(resource); } } -/// [`Command`] to log the components of a given entity. See [`EntityCommands::log_components`]. -pub struct LogComponents { - entity: Entity, -} - -impl Command for LogComponents { - fn apply(self, world: &mut World) { - let debug_infos: Vec<_> = world - .inspect_entity(self.entity) - .into_iter() - .map(|component_info| component_info.name()) - .collect(); - info!("Entity {:?}: {:?}", self.entity, debug_infos); - } +/// [`EntityCommand`] to log the components of a given entity. See [`EntityCommands::log_components`]. +fn log_components(entity: Entity, world: &mut World) { + let debug_infos: Vec<_> = world + .inspect_entity(entity) + .into_iter() + .map(|component_info| component_info.name()) + .collect(); + info!("Entity {:?}: {:?}", entity, debug_infos); } #[cfg(test)] diff --git a/crates/bevy_ecs/src/system/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index 2eb3c06941ed6..3c356c98175cf 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -6,6 +6,7 @@ use crate::{ }; use bevy_utils::all_tuples; use bevy_utils::synccell::SyncCell; +use std::marker::PhantomData; /// A parameter that can be used in an exclusive system (a system with an `&mut World` parameter). /// Any parameters implementing this trait must come after the `&mut World` parameter. @@ -70,6 +71,17 @@ impl<'_s, T: FromWorld + Send + 'static> ExclusiveSystemParam for Local<'_s, T> } } +impl ExclusiveSystemParam for PhantomData { + type State = (); + type Item<'s> = PhantomData; + + 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> { + PhantomData + } +} + macro_rules! impl_exclusive_system_param_tuple { ($($param: ident),*) => { #[allow(unused_variables)] @@ -98,3 +110,38 @@ macro_rules! impl_exclusive_system_param_tuple { } all_tuples!(impl_exclusive_system_param_tuple, 0, 16, P); + +#[cfg(test)] +mod tests { + use crate as bevy_ecs; + use crate::schedule::Schedule; + use crate::system::Local; + use crate::world::World; + use bevy_ecs_macros::Resource; + use std::marker::PhantomData; + + #[test] + fn test_exclusive_system_params() { + #[derive(Resource, Default)] + struct Res { + test_value: u32, + } + + fn my_system(world: &mut World, mut local: Local, _phantom: PhantomData>) { + assert_eq!(world.resource::().test_value, *local); + *local += 1; + world.resource_mut::().test_value += 1; + } + + let mut schedule = Schedule::default(); + schedule.add_systems(my_system); + + let mut world = World::default(); + world.init_resource::(); + + schedule.run(&mut world); + schedule.run(&mut world); + + assert_eq!(2, world.get_resource::().unwrap().test_value); + } +} diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 9056a1648153e..98f58b69242b5 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -111,7 +111,7 @@ impl SystemMeta { /// # Example /// /// Basic usage: -/// ```rust +/// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; @@ -144,7 +144,7 @@ impl SystemMeta { /// // You need to manually call `.apply(world)` on the `SystemState` to apply them. /// ``` /// Caching: -/// ```rust +/// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; @@ -576,7 +576,7 @@ where /// /// To create something like [`PipeSystem`], but in entirely safe code. /// -/// ```rust +/// ``` /// use std::num::ParseIntError; /// /// use bevy_ecs::prelude::*; diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index cebadc6f3220b..630eb22bdff1d 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -110,6 +110,7 @@ mod function_system; mod query; #[allow(clippy::module_inception)] mod system; +mod system_name; mod system_param; mod system_registry; @@ -123,6 +124,7 @@ pub use exclusive_system_param::*; pub use function_system::*; pub use query::*; pub use system::*; +pub use system_name::*; pub use system_param::*; pub use system_registry::*; @@ -308,6 +310,20 @@ pub fn assert_system_does_not_conflict std::ops::Deref for In { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for In { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + #[cfg(test)] mod tests { use std::any::TypeId; @@ -1717,14 +1733,14 @@ mod tests { let mut sched = Schedule::default(); sched.add_systems( ( - (|mut res: ResMut| { + |mut res: ResMut| { res.0 += 1; - }), - (|mut res: ResMut| { + }, + |mut res: ResMut| { res.0 += 2; - }), + }, ) - .distributive_run_if(resource_exists::().or_else(resource_exists::())), + .distributive_run_if(resource_exists::.or_else(resource_exists::)), ); sched.initialize(&mut world).unwrap(); sched.run(&mut world); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 72b4271dbca89..9f0eaf34d4a50 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -903,7 +903,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// This method panics if there is a query mismatch or a non-existing entity. /// /// # Examples - /// ```rust, no_run + /// ``` no_run /// use bevy_ecs::prelude::*; /// /// #[derive(Component)] @@ -1011,7 +1011,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// # Examples /// - /// ```rust, no_run + /// ``` no_run /// use bevy_ecs::prelude::*; /// /// #[derive(Component)] @@ -1431,6 +1431,96 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { .is_ok() } } + + /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// + /// 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) and + /// [`Changed`](crate::query::Changed) will not be respected. To maintain or change filter + /// terms see [`Self::transmute_lens_filtered`] + /// + /// ## Panics + /// + /// This will panic if `NewD` is not a subset of the original fetch `Q` + /// + /// ## Example + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::system::QueryLens; + /// # + /// # #[derive(Component)] + /// # struct A(usize); + /// # + /// # #[derive(Component)] + /// # struct B(usize); + /// # + /// # let mut world = World::new(); + /// # + /// # world.spawn((A(10), B(5))); + /// # + /// fn reusable_function(lens: &mut QueryLens<&A>) { + /// assert_eq!(lens.query().single().0, 10); + /// } + /// + /// // We can use the function in a system that takes the exact query. + /// fn system_1(mut query: Query<&A>) { + /// reusable_function(&mut query.as_query_lens()); + /// } + /// + /// // We can also use it with a query that does not match exactly + /// // by transmuting it. + /// fn system_2(mut query: Query<(&mut A, &B)>) { + /// let mut lens = query.transmute_lens::<&A>(); + /// reusable_function(&mut lens); + /// } + /// + /// # let mut schedule = Schedule::default(); + /// # schedule.add_systems((system_1, system_2)); + /// # schedule.run(&mut world); + /// ``` + /// + /// ## Allowed Transmutes + /// + /// Besides removing parameters from the query, you can also + /// make limited changes to the types of paramters. + /// + /// * Can always add/remove `Entity` + /// * `Ref` <-> `&T` + /// * `&mut T` -> `&T` + /// * `&mut T` -> `Ref` + /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) + /// + pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { + self.transmute_lens_filtered::() + } + + /// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type. + /// + /// 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) and + /// [`Changed`](crate::query::Changed) will only be respected if they are in the type signature. + pub fn transmute_lens_filtered( + &mut self, + ) -> QueryLens<'_, NewD, NewF> { + // SAFETY: There are no other active borrows of data from world + let world = unsafe { self.world.world() }; + let state = self.state.transmute_filtered::(world); + QueryLens { + world: self.world, + state, + last_run: self.last_run, + this_run: self.this_run, + force_read_only_component_access: self.force_read_only_component_access, + } + } + + /// Gets a [`QueryLens`] with the same accesses as the existing query + pub fn as_query_lens(&mut self) -> QueryLens<'_, D> { + self.transmute_lens() + } } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, F> { @@ -1532,3 +1622,43 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter> Query<'w, 's, D, F> { } } } + +/// Type returned from [`Query::transmute_lens`] containing the new [`QueryState`]. +/// +/// Call [`query`](QueryLens::query) or [`into`](Into::into) to construct the resulting [`Query`] +pub struct QueryLens<'w, Q: QueryData, F: QueryFilter = ()> { + world: UnsafeWorldCell<'w>, + state: QueryState, + last_run: Tick, + this_run: Tick, + force_read_only_component_access: bool, +} + +impl<'w, Q: QueryData, F: QueryFilter> QueryLens<'w, Q, F> { + /// Create a [`Query`] from the underlying [`QueryState`]. + pub fn query(&mut self) -> Query<'w, '_, Q, F> { + Query { + world: self.world, + state: &self.state, + last_run: self.last_run, + this_run: self.this_run, + force_read_only_component_access: self.force_read_only_component_access, + } + } +} + +impl<'w, 's, Q: QueryData, F: QueryFilter> From<&'s mut QueryLens<'w, Q, F>> + for Query<'w, 's, Q, F> +{ + fn from(value: &'s mut QueryLens<'w, Q, F>) -> Query<'w, 's, Q, F> { + value.query() + } +} + +impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> + for QueryLens<'q, Q, F> +{ + fn from(value: &'q mut Query<'w, '_, Q, F>) -> QueryLens<'q, Q, F> { + value.transmute_lens_filtered() + } +} diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs new file mode 100644 index 0000000000000..cc61542f795a0 --- /dev/null +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -0,0 +1,134 @@ +use crate::component::Tick; +use crate::prelude::World; +use crate::system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}; +use crate::world::unsafe_world_cell::UnsafeWorldCell; +use std::borrow::Cow; +use std::ops::Deref; + +/// [`SystemParam`] that returns the name of the system which it is used in. +/// +/// This is not a reliable identifier, it is more so useful for debugging or logging. +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::system::SystemName; +/// # use bevy_ecs::system::SystemParam; +/// +/// #[derive(SystemParam)] +/// struct Logger<'s> { +/// system_name: SystemName<'s>, +/// } +/// +/// impl<'s> Logger<'s> { +/// fn log(&mut self, message: &str) { +/// eprintln!("{}: {}", self.system_name, message); +/// } +/// } +/// +/// fn system1(mut logger: Logger) { +/// // Prints: "crate_name::mod_name::system1: Hello". +/// logger.log("Hello"); +/// } +/// ``` +#[derive(Debug)] +pub struct SystemName<'s>(&'s str); + +impl<'s> SystemName<'s> { + /// 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() + } +} + +impl<'s> AsRef for SystemName<'s> { + fn as_ref(&self) -> &str { + self.name() + } +} + +impl<'s> From> for &'s str { + fn from(name: SystemName<'s>) -> &'s str { + name.0 + } +} + +impl<'s> std::fmt::Display for SystemName<'s> { + #[inline(always)] + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(&self.name(), f) + } +} + +// SAFETY: no component value access +unsafe impl SystemParam for SystemName<'_> { + type State = Cow<'static, str>; + type Item<'w, 's> = SystemName<'s>; + + fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + system_meta.name.clone() + } + + #[inline] + unsafe fn get_param<'w, 's>( + name: &'s mut Self::State, + _system_meta: &SystemMeta, + _world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + SystemName(name) + } +} + +// SAFETY: Only reads internal system state +unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} + +impl ExclusiveSystemParam for SystemName<'_> { + type State = Cow<'static, str>; + type Item<'s> = SystemName<'s>; + + fn init(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + system_meta.name.clone() + } + + fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { + SystemName(state) + } +} + +#[cfg(test)] +mod tests { + use crate::system::SystemName; + use crate::world::World; + + #[test] + fn test_system_name_regular_param() { + fn testing(name: SystemName) -> String { + name.name().to_owned() + } + + let mut world = World::default(); + let id = world.register_system(testing); + let name = world.run_system(id).unwrap(); + assert!(name.ends_with("testing")); + } + + #[test] + fn test_system_name_exclusive_param() { + fn testing(_world: &mut World, name: SystemName) -> String { + name.name().to_owned() + } + + let mut world = World::default(); + let id = world.register_system(testing); + let name = world.run_system(id).unwrap(); + assert!(name.ends_with("testing")); + } +} diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 25706c30fd2c6..a3d5872c8a123 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -18,7 +18,6 @@ pub use bevy_ecs_macros::SystemParam; use bevy_ptr::UnsafeCellDeref; use bevy_utils::{all_tuples, synccell::SyncCell}; use std::{ - borrow::Cow, fmt::Debug, marker::PhantomData, ops::{Deref, DerefMut}, @@ -1287,69 +1286,6 @@ unsafe impl SystemParam for SystemChangeTick { } } -/// Name of the system that corresponds to this [`crate::system::SystemState`]. -/// -/// This is not a reliable identifier, it is more so useful for debugging -/// purposes of finding where a system parameter is being used incorrectly. -#[derive(Debug)] -pub struct SystemName<'s>(&'s str); - -impl<'s> SystemName<'s> { - /// 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() - } -} - -impl<'s> AsRef for SystemName<'s> { - fn as_ref(&self) -> &str { - self.name() - } -} - -impl<'s> From> for &'s str { - fn from(name: SystemName<'s>) -> &'s str { - name.0 - } -} - -impl<'s> std::fmt::Display for SystemName<'s> { - #[inline(always)] - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Display::fmt(&self.name(), f) - } -} - -// SAFETY: no component value access -unsafe impl SystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'w, 's> = SystemName<'s>; - - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() - } - - #[inline] - unsafe fn get_param<'w, 's>( - name: &'s mut Self::State, - _system_meta: &SystemMeta, - _world: UnsafeWorldCell<'w>, - _change_tick: Tick, - ) -> Self::Item<'w, 's> { - SystemName(name) - } -} - -// SAFETY: Only reads internal system state -unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} - macro_rules! impl_system_param_tuple { ($($param: ident),*) => { // SAFETY: tuple consists only of ReadOnlySystemParams diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 5a9d7269d1032..c04ff35b7c8f4 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -151,7 +151,7 @@ impl World { /// /// ## Running a system /// - /// ```rust + /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Resource, Default)] /// struct Counter(u8); @@ -171,7 +171,7 @@ impl World { /// /// ## Change detection /// - /// ```rust + /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Resource, Default)] /// struct ChangeDetector; @@ -195,7 +195,7 @@ impl World { /// /// ## Getting system output /// - /// ```rust + /// ``` /// # use bevy_ecs::prelude::*; /// /// #[derive(Resource)] @@ -243,7 +243,7 @@ impl World { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Resource, Default)] /// struct Counter(u8); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 7d21ae61a64f1..9558ff5c4e73e 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -4,6 +4,7 @@ use crate::{ change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, + query::{Access, DebugCheckedUnwrap}, removal_detection::RemovedComponentEvents, storage::Storages, world::{Mut, World}, @@ -190,7 +191,7 @@ impl<'a> From<&'a EntityMut<'_>> for EntityRef<'a> { /// Provides mutable access to a single entity and all of its components. /// -/// Contrast with [`EntityWorldMut`], with allows adding and removing components, +/// Contrast with [`EntityWorldMut`], which allows adding and removing components, /// despawning the entity, and provides mutable access to the entire world. /// Because of this, `EntityWorldMut` cannot coexist with any other world accesses. /// @@ -1495,6 +1496,358 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { } } +/// Provides read-only access to a single entity and some of its components defined by the contained [`Access`]. +#[derive(Clone)] +pub struct FilteredEntityRef<'w> { + entity: UnsafeEntityCell<'w>, + access: Access, +} + +impl<'w> FilteredEntityRef<'w> { + /// # Safety + /// - No `&mut World` can exist from the underlying `UnsafeWorldCell` + /// - If `access` takes read access to a component no mutable reference to that + /// component can exist at the same time as the returned [`FilteredEntityMut`] + /// - If `access` takes any access for a component `entity` must have that component. + pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access) -> Self { + Self { entity, access } + } + + /// Returns the [ID](Entity) of the current entity. + #[inline] + #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] + pub fn id(&self) -> Entity { + self.entity.id() + } + + /// Gets metadata indicating the location where the current entity is stored. + #[inline] + pub fn location(&self) -> EntityLocation { + self.entity.location() + } + + /// Returns the archetype that the current entity belongs to. + #[inline] + pub fn archetype(&self) -> &Archetype { + self.entity.archetype() + } + + /// Returns an iterator over the component ids that are accessed by self. + #[inline] + pub fn components(&self) -> impl Iterator + '_ { + self.access.reads_and_writes() + } + + /// Returns a reference to the underlying [`Access`]. + #[inline] + pub fn access(&self) -> &Access { + &self.access + } + + /// Returns `true` if the current entity has a component of type `T`. + /// Otherwise, this returns `false`. + /// + /// ## Notes + /// + /// If you do not know the concrete type of a component, consider using + /// [`Self::contains_id`] or [`Self::contains_type_id`]. + #[inline] + pub fn contains(&self) -> bool { + self.contains_type_id(TypeId::of::()) + } + + /// Returns `true` if the current entity has a component identified by `component_id`. + /// Otherwise, this returns false. + /// + /// ## Notes + /// + /// - If you know the concrete type of the component, you should prefer [`Self::contains`]. + /// - If you know the component's [`TypeId`] but not its [`ComponentId`], consider using + /// [`Self::contains_type_id`]. + #[inline] + pub fn contains_id(&self, component_id: ComponentId) -> bool { + self.entity.contains_id(component_id) + } + + /// Returns `true` if the current entity has a component with the type identified by `type_id`. + /// Otherwise, this returns false. + /// + /// ## Notes + /// + /// - If you know the concrete type of the component, you should prefer [`Self::contains`]. + /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. + #[inline] + pub fn contains_type_id(&self, type_id: TypeId) -> bool { + self.entity.contains_type_id(type_id) + } + + /// Gets access to the component of type `T` for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get(&self) -> Option<&'w T> { + let Some(id) = self.entity.world().components().get_id(TypeId::of::()) else { + return None; + }; + self.access + .has_read(id) + // SAFETY: We have read access so we must have the component + .then(|| unsafe { self.entity.get().debug_checked_unwrap() }) + } + + /// Gets access to the component of type `T` for the current entity, + /// including change detection information as a [`Ref`]. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get_ref(&self) -> Option> { + let Some(id) = self.entity.world().components().get_id(TypeId::of::()) else { + return None; + }; + self.access + .has_read(id) + // SAFETY: We have read access so we must have the component + .then(|| unsafe { self.entity.get_ref().debug_checked_unwrap() }) + } + + /// Retrieves the change ticks for the given component. This can be useful for implementing change + /// detection in custom runtimes. + #[inline] + pub fn get_change_ticks(&self) -> Option { + let Some(id) = self.entity.world().components().get_id(TypeId::of::()) else { + return None; + }; + self.access + .has_read(id) + // SAFETY: We have read access so we must have the component + .then(|| unsafe { self.entity.get_change_ticks::().debug_checked_unwrap() }) + } + + /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change + /// detection in custom runtimes. + /// + /// **You should prefer to use the typed API [`Self::get_change_ticks`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + #[inline] + pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { + // SAFETY: We have read access so we must have the component + self.access.has_read(component_id).then(|| unsafe { + self.entity + .get_change_ticks_by_id(component_id) + .debug_checked_unwrap() + }) + } + + /// Gets the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`Self::get`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`FilteredEntityRef::get`], this returns a raw pointer to the component, + /// which is only valid while the [`FilteredEntityRef`] is alive. + #[inline] + pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + self.access + .has_read(component_id) + // SAFETY: We have read access so we must have the component + .then(|| unsafe { self.entity.get_by_id(component_id).debug_checked_unwrap() }) + } +} + +impl<'w> From> for FilteredEntityRef<'w> { + fn from(entity_mut: FilteredEntityMut<'w>) -> Self { + // SAFETY: + // - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`. + unsafe { FilteredEntityRef::new(entity_mut.entity, entity_mut.access) } + } +} + +impl<'a> From<&'a FilteredEntityMut<'_>> for FilteredEntityRef<'a> { + fn from(entity_mut: &'a FilteredEntityMut<'_>) -> Self { + // SAFETY: + // - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`. + unsafe { FilteredEntityRef::new(entity_mut.entity, entity_mut.access.clone()) } + } +} + +/// Provides mutable access to a single entity and some of its components defined by the contained [`Access`]. +pub struct FilteredEntityMut<'w> { + entity: UnsafeEntityCell<'w>, + access: Access, +} + +impl<'w> FilteredEntityMut<'w> { + /// # Safety + /// - No `&mut World` can exist from the underlying `UnsafeWorldCell` + /// - If `access` takes read access to a component no mutable reference to that + /// component can exist at the same time as the returned [`FilteredEntityMut`] + /// - If `access` takes write access to a component, no reference to that component + /// may exist at the same time as the returned [`FilteredEntityMut`] + /// - If `access` takes any access for a component `entity` must have that component. + pub(crate) unsafe fn new(entity: UnsafeEntityCell<'w>, access: Access) -> Self { + Self { entity, access } + } + + /// Returns a new instance with a shorter lifetime. + /// This is useful if you have `&mut FilteredEntityMut`, but you need `FilteredEntityMut`. + pub fn reborrow(&mut self) -> FilteredEntityMut<'_> { + // SAFETY: We have exclusive access to the entire entity and its components. + unsafe { Self::new(self.entity, self.access.clone()) } + } + + /// Gets read-only access to all of the entity's components. + pub fn as_readonly(&self) -> FilteredEntityRef<'_> { + FilteredEntityRef::from(self) + } + + /// Returns the [ID](Entity) of the current entity. + #[inline] + #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] + pub fn id(&self) -> Entity { + self.entity.id() + } + + /// Gets metadata indicating the location where the current entity is stored. + #[inline] + pub fn location(&self) -> EntityLocation { + self.entity.location() + } + + /// Returns the archetype that the current entity belongs to. + #[inline] + pub fn archetype(&self) -> &Archetype { + self.entity.archetype() + } + + /// Returns an iterator over the component ids that are accessed by self. + #[inline] + pub fn components(&self) -> impl Iterator + '_ { + self.access.reads_and_writes() + } + + /// Returns a reference to the underlying [`Access`]. + #[inline] + pub fn access(&self) -> &Access { + &self.access + } + + /// Returns `true` if the current entity has a component of type `T`. + /// Otherwise, this returns `false`. + /// + /// ## Notes + /// + /// If you do not know the concrete type of a component, consider using + /// [`Self::contains_id`] or [`Self::contains_type_id`]. + #[inline] + pub fn contains(&self) -> bool { + self.contains_type_id(TypeId::of::()) + } + + /// Returns `true` if the current entity has a component identified by `component_id`. + /// Otherwise, this returns false. + /// + /// ## Notes + /// + /// - If you know the concrete type of the component, you should prefer [`Self::contains`]. + /// - If you know the component's [`TypeId`] but not its [`ComponentId`], consider using + /// [`Self::contains_type_id`]. + #[inline] + pub fn contains_id(&self, component_id: ComponentId) -> bool { + self.entity.contains_id(component_id) + } + + /// Returns `true` if the current entity has a component with the type identified by `type_id`. + /// Otherwise, this returns false. + /// + /// ## Notes + /// + /// - If you know the concrete type of the component, you should prefer [`Self::contains`]. + /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. + #[inline] + pub fn contains_type_id(&self, type_id: TypeId) -> bool { + self.entity.contains_type_id(type_id) + } + + /// Gets access to the component of type `T` for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get(&self) -> Option<&'_ T> { + self.as_readonly().get() + } + + /// Gets access to the component of type `T` for the current entity, + /// including change detection information as a [`Ref`]. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get_ref(&self) -> Option> { + self.as_readonly().get_ref() + } + + /// Gets mutable access to the component of type `T` for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get_mut(&mut self) -> Option> { + let Some(id) = self.entity.world().components().get_id(TypeId::of::()) else { + return None; + }; + self.access + .has_write(id) + // SAFETY: We have write access so we must have the component + .then(|| unsafe { self.entity.get_mut().debug_checked_unwrap() }) + } + + /// Retrieves the change ticks for the given component. This can be useful for implementing change + /// detection in custom runtimes. + #[inline] + pub fn get_change_ticks(&self) -> Option { + self.as_readonly().get_change_ticks::() + } + + /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change + /// detection in custom runtimes. + /// + /// **You should prefer to use the typed API [`Self::get_change_ticks`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + #[inline] + pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { + self.as_readonly().get_change_ticks_by_id(component_id) + } + + /// Gets the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`Self::get`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`FilteredEntityMut::get`], this returns a raw pointer to the component, + /// which is only valid while the [`FilteredEntityMut`] is alive. + #[inline] + pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + self.as_readonly().get_by_id(component_id) + } + + /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`Self::get_mut`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`FilteredEntityMut::get_mut`], this returns a raw pointer to the component, + /// which is only valid while the [`FilteredEntityMut`] is alive. + #[inline] + pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + // SAFETY: We have write access so we must have the component + self.access.has_write(component_id).then(|| unsafe { + self.entity + .get_mut_by_id(component_id) + .debug_checked_unwrap() + }) + } +} + /// Inserts a dynamic [`Bundle`] into the entity. /// /// # Safety diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index 2f5f2e9b914ef..b075205818d69 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -1,3 +1,4 @@ +use crate::system::{ExclusiveSystemParam, SystemMeta}; use crate::{ component::Tick, storage::SparseSetIndex, @@ -65,6 +66,19 @@ unsafe impl SystemParam for WorldId { } } +impl ExclusiveSystemParam for WorldId { + type State = WorldId; + type Item<'s> = WorldId; + + fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + world.id() + } + + fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { + *state + } +} + impl SparseSetIndex for WorldId { #[inline] fn sparse_set_index(&self) -> usize { @@ -95,6 +109,30 @@ mod tests { } } + #[test] + fn world_id_system_param() { + fn test_system(world_id: WorldId) -> WorldId { + world_id + } + + let mut world = World::default(); + let system_id = world.register_system(test_system); + let world_id = world.run_system(system_id).unwrap(); + assert_eq!(world.id(), world_id); + } + + #[test] + fn world_id_exclusive_system_param() { + fn test_system(_world: &mut World, world_id: WorldId) -> WorldId { + world_id + } + + let mut world = World::default(); + let system_id = world.register_system(test_system); + let world_id = world.run_system(system_id).unwrap(); + assert_eq!(world.id(), world_id); + } + // We cannot use this test as-is, as it causes other tests to panic due to using the same atomic variable. // #[test] // #[should_panic] diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 8fb7658bd8205..1bb0e74d0b04f 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -7,7 +7,10 @@ pub mod unsafe_world_cell; mod world_cell; pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}; -pub use entity_ref::{EntityMut, EntityRef, EntityWorldMut, Entry, OccupiedEntry, VacantEntry}; +pub use entity_ref::{ + EntityMut, EntityRef, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef, + OccupiedEntry, VacantEntry, +}; pub use spawn_batch::*; pub use world_cell::*; @@ -15,7 +18,10 @@ use crate::{ archetype::{ArchetypeComponentId, ArchetypeId, ArchetypeRow, Archetypes}, bundle::{Bundle, BundleInserter, BundleSpawner, Bundles}, change_detection::{MutUntyped, TicksMut}, - component::{Component, ComponentDescriptor, ComponentId, ComponentInfo, Components, Tick}, + component::{ + Component, ComponentDescriptor, ComponentId, ComponentInfo, ComponentTicks, Components, + Tick, + }, entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, event::{Event, EventId, Events, SendBatchIds}, query::{DebugCheckedUnwrap, QueryData, QueryEntityError, QueryFilter, QueryState}, @@ -207,7 +213,7 @@ impl World { /// Returns [`None`] if the `Component` type has not yet been initialized within /// the `World` using [`World::init_component`]. /// - /// ```rust + /// ``` /// use bevy_ecs::prelude::*; /// /// let mut world = World::new(); @@ -507,7 +513,7 @@ impl World { .iter() .enumerate() .map(|(archetype_row, archetype_entity)| { - let entity = archetype_entity.entity(); + let entity = archetype_entity.id(); let location = EntityLocation { archetype_id: archetype.id(), archetype_row: ArchetypeRow::new(archetype_row), @@ -536,7 +542,7 @@ impl World { .iter() .enumerate() .map(move |(archetype_row, archetype_entity)| { - let entity = archetype_entity.entity(); + let entity = archetype_entity.id(); let location = EntityLocation { archetype_id: archetype.id(), archetype_row: ArchetypeRow::new(archetype_row), @@ -1252,6 +1258,26 @@ impl World { .unwrap_or(false) } + /// Retrieves the change ticks for the given resource. + pub fn get_resource_change_ticks(&self) -> Option { + self.components + .get_resource_id(TypeId::of::()) + .and_then(|component_id| self.get_resource_change_ticks_by_id(component_id)) + } + + /// Retrieves the change ticks for the given [`ComponentId`]. + /// + /// **You should prefer to use the typed API [`World::get_resource_change_ticks`] where possible.** + pub fn get_resource_change_ticks_by_id( + &self, + component_id: ComponentId, + ) -> Option { + self.storages + .resources + .get(component_id) + .and_then(|resource| resource.get_ticks()) + } + /// Gets a reference to the resource of the given type /// /// # Panics diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.rs b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.rs new file mode 100644 index 0000000000000..e7d61d708e16e --- /dev/null +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.rs @@ -0,0 +1,39 @@ +use bevy_ecs::prelude::*; +use bevy_ecs::system::SystemState; + +#[derive(Component, Eq, PartialEq, Debug)] +struct Foo(u32); + +#[derive(Component)] +struct Bar; + +fn main() { + let mut world = World::default(); + world.spawn(Foo(10)); + + let mut system_state = SystemState::>::new(&mut world); + let mut query = system_state.get_mut(&mut world); + + { + let mut lens_a = query.transmute_lens::<&mut Foo>(); + let mut lens_b = query.transmute_lens::<&mut Foo>(); + + let mut query_a = lens_a.query(); + let mut query_b = lens_b.query(); + + let a = query_a.single_mut(); + let b = query_b.single_mut(); // oops 2 mutable references to same Foo + assert_eq!(*a, *b); + } + + { + let mut lens = query.transmute_lens::<&mut Foo>(); + + let mut query_a = lens.query(); + let mut query_b = lens.query(); + + let a = query_a.single_mut(); + let b = query_b.single_mut(); // oops 2 mutable references to same Foo + assert_eq!(*a, *b); + } +} diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.stderr new file mode 100644 index 0000000000000..4e8e283e39528 --- /dev/null +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_transmute_safety.stderr @@ -0,0 +1,21 @@ +error[E0499]: cannot borrow `query` as mutable more than once at a time + --> tests/ui/query_transmute_safety.rs:19:26 + | +18 | let mut lens_a = query.transmute_lens::<&mut Foo>(); + | ----- first mutable borrow occurs here +19 | let mut lens_b = query.transmute_lens::<&mut Foo>(); + | ^^^^^ second mutable borrow occurs here +20 | +21 | let mut query_a = lens_a.query(); + | ------ first borrow later used here + +error[E0499]: cannot borrow `lens` as mutable more than once at a time + --> tests/ui/query_transmute_safety.rs:33:27 + | +32 | let mut query_a = lens.query(); + | ---- first mutable borrow occurs here +33 | let mut query_b = lens.query(); + | ^^^^ second mutable borrow occurs here +34 | +35 | let a = query_a.single_mut(); + | ------- first borrow later used here diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 1f404d1bfc957..a76fd68389e4c 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -25,6 +25,8 @@ bevy_core = { path = "../bevy_core", version = "0.12.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.12.0" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.12.0" } bevy_transform = { path = "../bevy_transform", version = "0.12.0" } +bevy_log = { path = "../bevy_log", version = "0.12.0" } +bevy_gizmos_macros = { path = "macros", version = "0.12.0" } [lints] workspace = true diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml new file mode 100644 index 0000000000000..376d442ed3dfd --- /dev/null +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "bevy_gizmos_macros" +version = "0.12.0" +edition = "2021" +description = "Derive implementations for bevy_gizmos" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.12.0" } + +syn = "2.0" +proc-macro2 = "1.0" +quote = "1.0" diff --git a/crates/bevy_gizmos/macros/src/lib.rs b/crates/bevy_gizmos/macros/src/lib.rs new file mode 100644 index 0000000000000..46425ead704b7 --- /dev/null +++ b/crates/bevy_gizmos/macros/src/lib.rs @@ -0,0 +1,23 @@ +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, parse_quote, DeriveInput, Path}; + +#[proc_macro_derive(GizmoConfigGroup)] +pub fn derive_gizmo_config_group(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_gizmos_path: Path = BevyManifest::default().get_path("bevy_gizmos"); + let bevy_reflect_path: Path = BevyManifest::default().get_path("bevy_reflect"); + + ast.generics.make_where_clause().predicates.push( + parse_quote! { Self: #bevy_reflect_path::Reflect + #bevy_reflect_path::TypePath + Default}, + ); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_gizmos_path::config::GizmoConfigGroup for #struct_name #type_generics #where_clause { + } + }) +} diff --git a/crates/bevy_gizmos/src/aabb.rs b/crates/bevy_gizmos/src/aabb.rs new file mode 100644 index 0000000000000..ed9eccae027b4 --- /dev/null +++ b/crates/bevy_gizmos/src/aabb.rs @@ -0,0 +1,120 @@ +//! A module adding debug visualization of [`Aabb`]s. + +use crate as bevy_gizmos; + +use bevy_app::{Plugin, PostUpdate}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::Without, + reflect::ReflectComponent, + schedule::IntoSystemConfigs, + system::{Query, Res}, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{color::Color, primitives::Aabb}; +use bevy_transform::{ + components::{GlobalTransform, Transform}, + TransformSystem, +}; + +use crate::{ + config::{GizmoConfigGroup, GizmoConfigStore}, + gizmos::Gizmos, + AppGizmoBuilder, +}; + +/// A [`Plugin`] that provides visualization of [`Aabb`]s for debugging. +pub struct AabbGizmoPlugin; + +impl Plugin for AabbGizmoPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.register_type::() + .init_gizmo_group::() + .add_systems( + PostUpdate, + ( + draw_aabbs, + draw_all_aabbs.run_if(|config: Res| { + config.config::().1.draw_all + }), + ) + .after(TransformSystem::TransformPropagate), + ); + } +} +/// The [`GizmoConfigGroup`] used for debug visualizations of [`Aabb`] components on entities +#[derive(Clone, Default, Reflect, GizmoConfigGroup)] +pub struct AabbGizmoConfigGroup { + /// Draws all bounding boxes in the scene when set to `true`. + /// + /// To draw a specific entity's bounding box, you can add the [`ShowAabbGizmo`] component. + /// + /// Defaults to `false`. + pub draw_all: bool, + /// The default color for bounding box gizmos. + /// + /// A random color is chosen per box if `None`. + /// + /// Defaults to `None`. + pub default_color: Option, +} + +/// Add this [`Component`] to an entity to draw its [`Aabb`] component. +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component, Default)] +pub struct ShowAabbGizmo { + /// The color of the box. + /// + /// The default color from the [`AabbGizmoConfigGroup`] config is used if `None`, + pub color: Option, +} + +fn draw_aabbs( + query: Query<(Entity, &Aabb, &GlobalTransform, &ShowAabbGizmo)>, + mut gizmos: Gizmos, +) { + for (entity, &aabb, &transform, gizmo) in &query { + let color = gizmo + .color + .or(gizmos.config_ext.default_color) + .unwrap_or_else(|| color_from_entity(entity)); + gizmos.cuboid(aabb_transform(aabb, transform), color); + } +} + +fn draw_all_aabbs( + query: Query<(Entity, &Aabb, &GlobalTransform), Without>, + mut gizmos: Gizmos, +) { + for (entity, &aabb, &transform) in &query { + let color = gizmos + .config_ext + .default_color + .unwrap_or_else(|| color_from_entity(entity)); + gizmos.cuboid(aabb_transform(aabb, transform), color); + } +} + +fn color_from_entity(entity: Entity) -> Color { + let index = entity.index(); + + // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ + // + // See https://en.wikipedia.org/wiki/Low-discrepancy_sequence + // Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range, + // so that the closer the numbers are, the larger the difference of their image. + const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up + const RATIO_360: f32 = 360.0 / u32::MAX as f32; + let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360; + + Color::hsl(hue, 1., 0.5) +} + +fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform { + transform + * GlobalTransform::from( + Transform::from_translation(aabb.center.into()) + .with_scale((aabb.half_extents * 2.).into()), + ) +} diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index e84a0d9c885f3..27f698e00c546 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -4,12 +4,12 @@ //! and assorted support items. use crate::circles::DEFAULT_CIRCLE_SEGMENTS; -use crate::prelude::Gizmos; +use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_math::Vec2; use bevy_render::color::Color; use std::f32::consts::TAU; -impl<'s> Gizmos<'s> { +impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// Draw an arc, which is a part of the circumference of a circle, in 2D. /// /// This should be called for each frame the arc needs to be rendered. @@ -46,7 +46,7 @@ impl<'s> Gizmos<'s> { arc_angle: f32, radius: f32, color: Color, - ) -> Arc2dBuilder<'_, 's> { + ) -> Arc2dBuilder<'_, 'w, 's, T> { Arc2dBuilder { gizmos: self, position, @@ -60,8 +60,8 @@ impl<'s> Gizmos<'s> { } /// A builder returned by [`Gizmos::arc_2d`]. -pub struct Arc2dBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +pub struct Arc2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec2, direction_angle: f32, arc_angle: f32, @@ -70,7 +70,7 @@ pub struct Arc2dBuilder<'a, 's> { segments: Option, } -impl Arc2dBuilder<'_, '_> { +impl Arc2dBuilder<'_, '_, '_, T> { /// Set the number of line-segments for this arc. pub fn segments(mut self, segments: usize) -> Self { self.segments = Some(segments); @@ -78,8 +78,11 @@ impl Arc2dBuilder<'_, '_> { } } -impl Drop for Arc2dBuilder<'_, '_> { +impl Drop for Arc2dBuilder<'_, '_, '_, T> { fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } let segments = match self.segments { Some(segments) => segments, // Do a linear interpolation between 1 and `DEFAULT_CIRCLE_SEGMENTS` @@ -88,7 +91,7 @@ impl Drop for Arc2dBuilder<'_, '_> { }; let positions = arc_inner(self.direction_angle, self.arc_angle, self.radius, segments) - .map(|vec2| (vec2 + self.position)); + .map(|vec2| vec2 + self.position); self.gizmos.linestrip_2d(positions, self.color); } } diff --git a/crates/bevy_gizmos/src/arrows.rs b/crates/bevy_gizmos/src/arrows.rs index 9a1325c821302..b5416e412f558 100644 --- a/crates/bevy_gizmos/src/arrows.rs +++ b/crates/bevy_gizmos/src/arrows.rs @@ -3,20 +3,20 @@ //! Includes the implementation of [`Gizmos::arrow`] and [`Gizmos::arrow_2d`], //! and assorted support items. -use crate::prelude::Gizmos; +use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_math::{Quat, Vec2, Vec3}; use bevy_render::color::Color; /// A builder returned by [`Gizmos::arrow`] and [`Gizmos::arrow_2d`] -pub struct ArrowBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +pub struct ArrowBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, start: Vec3, end: Vec3, color: Color, tip_length: f32, } -impl ArrowBuilder<'_, '_> { +impl ArrowBuilder<'_, '_, '_, T> { /// Change the length of the tips to be `length`. /// The default tip length is [length of the arrow]/10. /// @@ -37,9 +37,12 @@ impl ArrowBuilder<'_, '_> { } } -impl Drop for ArrowBuilder<'_, '_> { +impl Drop for ArrowBuilder<'_, '_, '_, T> { /// Draws the arrow, by drawing lines with the stored [`Gizmos`] fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } // first, draw the body of the arrow self.gizmos.line(self.start, self.end, self.color); // now the hard part is to draw the head in a sensible way @@ -63,7 +66,7 @@ impl Drop for ArrowBuilder<'_, '_> { } } -impl<'s> Gizmos<'s> { +impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// Draw an arrow in 3D, from `start` to `end`. Has four tips for convienent viewing from any direction. /// /// This should be called for each frame the arrow needs to be rendered. @@ -78,7 +81,7 @@ impl<'s> Gizmos<'s> { /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` - pub fn arrow(&mut self, start: Vec3, end: Vec3, color: Color) -> ArrowBuilder<'_, 's> { + pub fn arrow(&mut self, start: Vec3, end: Vec3, color: Color) -> ArrowBuilder<'_, 'w, 's, T> { let length = (end - start).length(); ArrowBuilder { gizmos: self, @@ -103,7 +106,12 @@ impl<'s> Gizmos<'s> { /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` - pub fn arrow_2d(&mut self, start: Vec2, end: Vec2, color: Color) -> ArrowBuilder<'_, 's> { + pub fn arrow_2d( + &mut self, + start: Vec2, + end: Vec2, + color: Color, + ) -> ArrowBuilder<'_, 'w, 's, T> { self.arrow(start.extend(0.), end.extend(0.), color) } } diff --git a/crates/bevy_gizmos/src/circles.rs b/crates/bevy_gizmos/src/circles.rs index 1b4948b198c1c..2143996907ddb 100644 --- a/crates/bevy_gizmos/src/circles.rs +++ b/crates/bevy_gizmos/src/circles.rs @@ -3,8 +3,8 @@ //! Includes the implementation of [`Gizmos::circle`] and [`Gizmos::circle_2d`], //! and assorted support items. -use crate::prelude::Gizmos; -use bevy_math::{Quat, Vec2, Vec3}; +use crate::prelude::{GizmoConfigGroup, Gizmos}; +use bevy_math::{primitives::Direction3d, Quat, Vec2, Vec3}; use bevy_render::color::Color; use std::f32::consts::TAU; @@ -17,7 +17,7 @@ fn circle_inner(radius: f32, segments: usize) -> impl Iterator { }) } -impl<'s> Gizmos<'s> { +impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// Draw a circle in 3D at `position` with the flat side facing `normal`. /// /// This should be called for each frame the circle needs to be rendered. @@ -28,12 +28,12 @@ impl<'s> Gizmos<'s> { /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// fn system(mut gizmos: Gizmos) { - /// gizmos.circle(Vec3::ZERO, Vec3::Z, 1., Color::GREEN); + /// gizmos.circle(Vec3::ZERO, Direction3d::Z, 1., Color::GREEN); /// /// // Circles have 32 line-segments by default. /// // You may want to increase this for larger circles. /// gizmos - /// .circle(Vec3::ZERO, Vec3::Z, 5., Color::RED) + /// .circle(Vec3::ZERO, Direction3d::Z, 5., Color::RED) /// .segments(64); /// } /// # bevy_ecs::system::assert_is_system(system); @@ -42,10 +42,10 @@ impl<'s> Gizmos<'s> { pub fn circle( &mut self, position: Vec3, - normal: Vec3, + normal: Direction3d, radius: f32, color: Color, - ) -> CircleBuilder<'_, 's> { + ) -> CircleBuilder<'_, 'w, 's, T> { CircleBuilder { gizmos: self, position, @@ -82,7 +82,7 @@ impl<'s> Gizmos<'s> { position: Vec2, radius: f32, color: Color, - ) -> Circle2dBuilder<'_, 's> { + ) -> Circle2dBuilder<'_, 'w, 's, T> { Circle2dBuilder { gizmos: self, position, @@ -94,16 +94,16 @@ impl<'s> Gizmos<'s> { } /// A builder returned by [`Gizmos::circle`]. -pub struct CircleBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +pub struct CircleBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec3, - normal: Vec3, + normal: Direction3d, radius: f32, color: Color, segments: usize, } -impl CircleBuilder<'_, '_> { +impl CircleBuilder<'_, '_, '_, T> { /// Set the number of line-segments for this circle. pub fn segments(mut self, segments: usize) -> Self { self.segments = segments; @@ -111,25 +111,28 @@ impl CircleBuilder<'_, '_> { } } -impl Drop for CircleBuilder<'_, '_> { +impl Drop for CircleBuilder<'_, '_, '_, T> { fn drop(&mut self) { - let rotation = Quat::from_rotation_arc(Vec3::Z, self.normal); + if !self.gizmos.enabled { + return; + } + let rotation = Quat::from_rotation_arc(Vec3::Z, *self.normal); let positions = circle_inner(self.radius, self.segments) - .map(|vec2| (self.position + rotation * vec2.extend(0.))); + .map(|vec2| self.position + rotation * vec2.extend(0.)); self.gizmos.linestrip(positions, self.color); } } /// A builder returned by [`Gizmos::circle_2d`]. -pub struct Circle2dBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +pub struct Circle2dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec2, radius: f32, color: Color, segments: usize, } -impl Circle2dBuilder<'_, '_> { +impl Circle2dBuilder<'_, '_, '_, T> { /// Set the number of line-segments for this circle. pub fn segments(mut self, segments: usize) -> Self { self.segments = segments; @@ -137,9 +140,12 @@ impl Circle2dBuilder<'_, '_> { } } -impl Drop for Circle2dBuilder<'_, '_> { +impl Drop for Circle2dBuilder<'_, '_, '_, T> { fn drop(&mut self) { - let positions = circle_inner(self.radius, self.segments).map(|vec2| (vec2 + self.position)); + if !self.gizmos.enabled { + return; + } + let positions = circle_inner(self.radius, self.segments).map(|vec2| vec2 + self.position); self.gizmos.linestrip_2d(positions, self.color); } } diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs new file mode 100644 index 0000000000000..c2656268c2342 --- /dev/null +++ b/crates/bevy_gizmos/src/config.rs @@ -0,0 +1,163 @@ +//! A module for the [`GizmoConfig`] [`Resource`]. + +use crate as bevy_gizmos; +pub use bevy_gizmos_macros::GizmoConfigGroup; + +use bevy_ecs::{component::Component, system::Resource}; +use bevy_reflect::{Reflect, TypePath}; +use bevy_render::view::RenderLayers; +use bevy_utils::HashMap; +use core::panic; +use std::{ + any::TypeId, + ops::{Deref, DerefMut}, +}; + +/// A trait used to create gizmo configs groups. +/// +/// Here you can store additional configuration for you gizmo group not covered by [`GizmoConfig`] +/// +/// Make sure to derive [`Default`] + [`Reflect`] and register in the app using `app.init_gizmo_group::()` +pub trait GizmoConfigGroup: Reflect + TypePath + Default {} + +/// The default gizmo config group. +#[derive(Default, Reflect, GizmoConfigGroup)] +pub struct DefaultGizmoConfigGroup; + +/// A [`Resource`] storing [`GizmoConfig`] and [`GizmoConfigGroup`] structs +/// +/// Use `app.init_gizmo_group::()` to register a custom config group. +#[derive(Resource, Default)] +pub struct GizmoConfigStore { + // INVARIANT: must map TypeId::of::() to correct type T + store: HashMap)>, +} + +impl GizmoConfigStore { + /// Returns [`GizmoConfig`] and [`GizmoConfigGroup`] associated with [`TypeId`] of a [`GizmoConfigGroup`] + pub fn get_config_dyn(&self, config_type_id: &TypeId) -> Option<(&GizmoConfig, &dyn Reflect)> { + let (config, ext) = self.store.get(config_type_id)?; + Some((config, ext.deref())) + } + + /// Returns [`GizmoConfig`] and [`GizmoConfigGroup`] associated with [`GizmoConfigGroup`] `T` + pub fn config(&self) -> (&GizmoConfig, &T) { + let Some((config, ext)) = self.get_config_dyn(&TypeId::of::()) else { + panic!("Requested config {} does not exist in `GizmoConfigStore`! Did you forget to add it using `app.init_gizmo_group()`?", T::type_path()); + }; + // hash map invariant guarantees that &dyn Reflect is of correct type T + let ext = ext.as_any().downcast_ref().unwrap(); + (config, ext) + } + + /// Returns mutable [`GizmoConfig`] and [`GizmoConfigGroup`] associated with [`TypeId`] of a [`GizmoConfigGroup`] + pub fn get_config_mut_dyn( + &mut self, + config_type_id: &TypeId, + ) -> Option<(&mut GizmoConfig, &mut dyn Reflect)> { + let (config, ext) = self.store.get_mut(config_type_id)?; + Some((config, ext.deref_mut())) + } + + /// Returns mutable [`GizmoConfig`] and [`GizmoConfigGroup`] associated with [`GizmoConfigGroup`] `T` + pub fn config_mut(&mut self) -> (&mut GizmoConfig, &mut T) { + let Some((config, ext)) = self.get_config_mut_dyn(&TypeId::of::()) else { + panic!("Requested config {} does not exist in `GizmoConfigStore`! Did you forget to add it using `app.init_gizmo_group()`?", T::type_path()); + }; + // hash map invariant guarantees that &dyn Reflect is of correct type T + let ext = ext.as_any_mut().downcast_mut().unwrap(); + (config, ext) + } + + /// Returns an iterator over all [`GizmoConfig`]s. + pub fn iter(&self) -> impl Iterator + '_ { + self.store + .iter() + .map(|(id, (config, ext))| (id, config, ext.deref())) + } + + /// Returns an iterator over all [`GizmoConfig`]s, by mutable reference. + pub fn iter_mut( + &mut self, + ) -> impl Iterator + '_ { + self.store + .iter_mut() + .map(|(id, (config, ext))| (id, config, ext.deref_mut())) + } + + /// Inserts [`GizmoConfig`] and [`GizmoConfigGroup`] replacing old values + pub fn insert(&mut self, config: GizmoConfig, ext_config: T) { + // INVARIANT: hash map must correctly map TypeId::of::() to &dyn Reflect of type T + self.store + .insert(TypeId::of::(), (config, Box::new(ext_config))); + } + + pub(crate) fn register(&mut self) { + self.insert(GizmoConfig::default(), T::default()); + } +} + +/// A struct that stores configuration for gizmos. +#[derive(Clone, Reflect)] +pub struct GizmoConfig { + /// Set to `false` to stop drawing gizmos. + /// + /// Defaults to `true`. + pub enabled: bool, + /// Line width specified in pixels. + /// + /// If `line_perspective` is `true` then this is the size in pixels at the camera's near plane. + /// + /// Defaults to `2.0`. + pub line_width: f32, + /// Apply perspective to gizmo lines. + /// + /// This setting only affects 3D, non-orthographic cameras. + /// + /// Defaults to `false`. + pub line_perspective: bool, + /// How closer to the camera than real geometry the line should be. + /// + /// In 2D this setting has no effect and is effectively always -1. + /// + /// Value between -1 and 1 (inclusive). + /// * 0 means that there is no change to the line position when rendering + /// * 1 means it is furthest away from camera as possible + /// * -1 means that it will always render in front of other things. + /// + /// This is typically useful if you are drawing wireframes on top of polygons + /// and your wireframe is z-fighting (flickering on/off) with your main model. + /// You would set this value to a negative number close to 0. + pub depth_bias: f32, + /// Describes which rendering layers gizmos will be rendered to. + /// + /// Gizmos will only be rendered to cameras with intersecting layers. + pub render_layers: RenderLayers, +} + +impl Default for GizmoConfig { + fn default() -> Self { + Self { + enabled: true, + line_width: 2., + line_perspective: false, + depth_bias: 0., + render_layers: Default::default(), + } + } +} + +#[derive(Component)] +pub(crate) struct GizmoMeshConfig { + pub line_perspective: bool, + pub render_layers: RenderLayers, +} + +impl From<&GizmoConfig> for GizmoMeshConfig { + fn from(item: &GizmoConfig) -> Self { + GizmoMeshConfig { + line_perspective: item.line_perspective, + render_layers: item.render_layers, + } + } +} diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index fa2cb10351b26..b9c0662879d1c 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -1,25 +1,33 @@ //! A module for the [`Gizmos`] [`SystemParam`]. -use std::iter; +use std::{iter, marker::PhantomData}; use crate::circles::DEFAULT_CIRCLE_SEGMENTS; use bevy_ecs::{ - system::{Deferred, Resource, SystemBuffer, SystemMeta, SystemParam}, - world::World, + component::Tick, + system::{Deferred, ReadOnlySystemParam, Res, Resource, SystemBuffer, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, World}, }; -use bevy_math::{Mat2, Quat, Vec2, Vec3}; +use bevy_math::{primitives::Direction3d, Mat2, Quat, Vec2, Vec3}; use bevy_render::color::Color; use bevy_transform::TransformPoint; +use crate::{ + config::GizmoConfigGroup, + config::{DefaultGizmoConfigGroup, GizmoConfigStore}, + prelude::GizmoConfig, +}; + type PositionItem = [f32; 3]; type ColorItem = [f32; 4]; #[derive(Resource, Default)] -pub(crate) struct GizmoStorage { +pub(crate) struct GizmoStorage { pub list_positions: Vec, pub list_colors: Vec, pub strip_positions: Vec, pub strip_colors: Vec, + marker: PhantomData, } /// A [`SystemParam`] for drawing gizmos. @@ -27,22 +35,82 @@ pub(crate) struct GizmoStorage { /// They are drawn in immediate mode, which means they will be rendered only for /// the frames in which they are spawned. /// Gizmos should be spawned before the [`Last`](bevy_app::Last) schedule to ensure they are drawn. -#[derive(SystemParam)] -pub struct Gizmos<'s> { - buffer: Deferred<'s, GizmoBuffer>, +pub struct Gizmos<'w, 's, T: GizmoConfigGroup = DefaultGizmoConfigGroup> { + buffer: Deferred<'s, GizmoBuffer>, + pub(crate) enabled: bool, + /// The currently used [`GizmoConfig`] + pub config: &'w GizmoConfig, + /// The currently used [`GizmoConfigGroup`] + pub config_ext: &'w T, +} + +type GizmosState = ( + Deferred<'static, GizmoBuffer>, + Res<'static, GizmoConfigStore>, +); +#[doc(hidden)] +pub struct GizmosFetchState { + state: as SystemParam>::State, +} +// SAFETY: All methods are delegated to existing `SystemParam` implemntations +unsafe impl SystemParam for Gizmos<'_, '_, T> { + type State = GizmosFetchState; + type Item<'w, 's> = Gizmos<'w, 's, T>; + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + GizmosFetchState { + state: GizmosState::::init_state(world, system_meta), + } + } + fn new_archetype( + state: &mut Self::State, + archetype: &bevy_ecs::archetype::Archetype, + system_meta: &mut SystemMeta, + ) { + GizmosState::::new_archetype(&mut state.state, archetype, system_meta); + } + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + GizmosState::::apply(&mut state.state, system_meta, world); + } + unsafe fn get_param<'w, 's>( + state: &'s mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + change_tick: Tick, + ) -> Self::Item<'w, 's> { + let (f0, f1) = + GizmosState::::get_param(&mut state.state, system_meta, world, change_tick); + // Accessing the GizmoConfigStore in the immediate mode API reduces performance significantly. + // Implementing SystemParam manually allows us to do it to here + // Having config available allows for early returns when gizmos are disabled + let (config, config_ext) = f1.into_inner().config::(); + Gizmos { + buffer: f0, + enabled: config.enabled, + config, + config_ext, + } + } +} +// Safety: Each field is `ReadOnlySystemParam`, and Gizmos SystemParam does not mutate world +unsafe impl<'w, 's, T: GizmoConfigGroup> ReadOnlySystemParam for Gizmos<'w, 's, T> +where + Deferred<'s, GizmoBuffer>: ReadOnlySystemParam, + Res<'w, GizmoConfigStore>: ReadOnlySystemParam, +{ } #[derive(Default)] -struct GizmoBuffer { +struct GizmoBuffer { list_positions: Vec, list_colors: Vec, strip_positions: Vec, strip_colors: Vec, + marker: PhantomData, } -impl SystemBuffer for GizmoBuffer { +impl SystemBuffer for GizmoBuffer { fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) { - let mut storage = world.resource_mut::(); + let mut storage = world.resource_mut::>(); storage.list_positions.append(&mut self.list_positions); storage.list_colors.append(&mut self.list_colors); storage.strip_positions.append(&mut self.strip_positions); @@ -50,7 +118,7 @@ impl SystemBuffer for GizmoBuffer { } } -impl<'s> Gizmos<'s> { +impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// Draw a line in 3D from `start` to `end`. /// /// This should be called for each frame the line needs to be rendered. @@ -67,6 +135,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn line(&mut self, start: Vec3, end: Vec3, color: Color) { + if !self.enabled { + return; + } self.extend_list_positions([start, end]); self.add_list_color(color, 2); } @@ -87,6 +158,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn line_gradient(&mut self, start: Vec3, end: Vec3, start_color: Color, end_color: Color) { + if !self.enabled { + return; + } self.extend_list_positions([start, end]); self.extend_list_colors([start_color, end_color]); } @@ -107,6 +181,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn ray(&mut self, start: Vec3, vector: Vec3, color: Color) { + if !self.enabled { + return; + } self.line(start, start + vector, color); } @@ -132,6 +209,9 @@ impl<'s> Gizmos<'s> { start_color: Color, end_color: Color, ) { + if !self.enabled { + return; + } self.line_gradient(start, start + vector, start_color, end_color); } @@ -151,6 +231,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn linestrip(&mut self, positions: impl IntoIterator, color: Color) { + if !self.enabled { + return; + } self.extend_strip_positions(positions); let len = self.buffer.strip_positions.len(); self.buffer @@ -179,6 +262,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn linestrip_gradient(&mut self, points: impl IntoIterator) { + if !self.enabled { + return; + } let points = points.into_iter(); let GizmoBuffer { @@ -227,7 +313,7 @@ impl<'s> Gizmos<'s> { rotation: Quat, radius: f32, color: Color, - ) -> SphereBuilder<'_, 's> { + ) -> SphereBuilder<'_, 'w, 's, T> { SphereBuilder { gizmos: self, position, @@ -254,6 +340,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn rect(&mut self, position: Vec3, rotation: Quat, size: Vec2, color: Color) { + if !self.enabled { + return; + } let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2.extend(0.)); self.linestrip([tl, tr, br, bl, tl], color); } @@ -274,6 +363,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn cuboid(&mut self, transform: impl TransformPoint, color: Color) { + if !self.enabled { + return; + } let rect = rect_inner(Vec2::ONE); // Front let [tlf, trf, brf, blf] = rect.map(|vec2| transform.transform_point(vec2.extend(0.5))); @@ -309,6 +401,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn line_2d(&mut self, start: Vec2, end: Vec2, color: Color) { + if !self.enabled { + return; + } self.line(start.extend(0.), end.extend(0.), color); } @@ -334,6 +429,9 @@ impl<'s> Gizmos<'s> { start_color: Color, end_color: Color, ) { + if !self.enabled { + return; + } self.line_gradient(start.extend(0.), end.extend(0.), start_color, end_color); } @@ -353,6 +451,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn linestrip_2d(&mut self, positions: impl IntoIterator, color: Color) { + if !self.enabled { + return; + } self.linestrip(positions.into_iter().map(|vec2| vec2.extend(0.)), color); } @@ -376,6 +477,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn linestrip_gradient_2d(&mut self, positions: impl IntoIterator) { + if !self.enabled { + return; + } self.linestrip_gradient( positions .into_iter() @@ -399,6 +503,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn ray_2d(&mut self, start: Vec2, vector: Vec2, color: Color) { + if !self.enabled { + return; + } self.line_2d(start, start + vector, color); } @@ -424,6 +531,9 @@ impl<'s> Gizmos<'s> { start_color: Color, end_color: Color, ) { + if !self.enabled { + return; + } self.line_gradient_2d(start, start + vector, start_color, end_color); } @@ -443,6 +553,9 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn rect_2d(&mut self, position: Vec2, rotation: f32, size: Vec2, color: Color) { + if !self.enabled { + return; + } let rotation = Mat2::from_angle(rotation); let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2); self.linestrip_2d([tl, tr, br, bl, tl], color); @@ -481,8 +594,8 @@ impl<'s> Gizmos<'s> { } /// A builder returned by [`Gizmos::sphere`]. -pub struct SphereBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +pub struct SphereBuilder<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec3, rotation: Quat, radius: f32, @@ -490,7 +603,7 @@ pub struct SphereBuilder<'a, 's> { circle_segments: usize, } -impl SphereBuilder<'_, '_> { +impl SphereBuilder<'_, '_, '_, T> { /// Set the number of line-segments per circle for this sphere. pub fn circle_segments(mut self, segments: usize) -> Self { self.circle_segments = segments; @@ -498,11 +611,19 @@ impl SphereBuilder<'_, '_> { } } -impl Drop for SphereBuilder<'_, '_> { +impl Drop for SphereBuilder<'_, '_, '_, T> { fn drop(&mut self) { + if !self.gizmos.enabled { + return; + } for axis in Vec3::AXES { self.gizmos - .circle(self.position, self.rotation * axis, self.radius, self.color) + .circle( + self.position, + Direction3d::new_unchecked(self.rotation * axis), + self.radius, + self.color, + ) .segments(self.circle_segments); } } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 26864664adc3a..8696477ac7e41 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -13,11 +13,24 @@ //! # bevy_ecs::system::assert_is_system(system); //! ``` //! -//! See the documentation on [`Gizmos`] for more examples. +//! See the documentation on [Gizmos](crate::gizmos::Gizmos) for more examples. + +/// Label for the the render systems handling the +#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)] +pub enum GizmoRenderSystem { + /// Adds gizmos to the [`Transparent2d`](bevy_core_pipeline::core_2d::Transparent2d) render phase + #[cfg(feature = "bevy_sprite")] + QueueLineGizmos2d, + /// Adds gizmos to the [`Transparent3d`](bevy_core_pipeline::core_3d::Transparent3d) render phase + #[cfg(feature = "bevy_pbr")] + QueueLineGizmos3d, +} +pub mod aabb; pub mod arcs; pub mod arrows; pub mod circles; +pub mod config; pub mod gizmos; #[cfg(feature = "bevy_sprite")] @@ -28,30 +41,34 @@ mod pipeline_3d; /// The `bevy_gizmos` prelude. pub mod prelude { #[doc(hidden)] - pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig}; + pub use crate::{ + aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}, + config::{DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore}, + gizmos::Gizmos, + AppGizmoBuilder, + }; } -use bevy_app::{Last, Plugin, PostUpdate}; +use aabb::AabbGizmoPlugin; +use bevy_app::{App, Last, Plugin}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; use bevy_core::cast_slice; use bevy_ecs::{ - change_detection::DetectChanges, component::Component, - entity::Entity, - query::{ROQueryItem, Without}, - reflect::{ReflectComponent, ReflectResource}, - schedule::IntoSystemConfigs, + query::ROQueryItem, + schedule::{IntoSystemConfigs, SystemSet}, system::{ lifetimeless::{Read, SRes}, - Commands, Query, Res, ResMut, Resource, SystemParamItem, + Commands, Res, ResMut, Resource, SystemParamItem, }, }; -use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; +use bevy_reflect::TypePath; use bevy_render::{ - color::Color, extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, - primitives::Aabb, - render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, + render_asset::{ + PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy, RenderAssetPlugin, + RenderAssets, + }, render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::{ binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout, @@ -59,15 +76,14 @@ use bevy_render::{ ShaderType, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode, }, renderer::RenderDevice, - view::RenderLayers, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_transform::{ - components::{GlobalTransform, Transform}, - TransformSystem, +use bevy_utils::{tracing::warn, HashMap}; +use config::{ + DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoMeshConfig, }; -use gizmos::{GizmoStorage, Gizmos}; -use std::mem; +use gizmos::GizmoStorage; +use std::{any::TypeId, mem}; const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(7414812689238026784); @@ -76,36 +92,29 @@ pub struct GizmoPlugin; impl Plugin for GizmoPlugin { fn build(&self, app: &mut bevy_app::App) { + // Gizmos cannot work without either a 3D or 2D renderer. + #[cfg(all(not(feature = "bevy_pbr"), not(feature = "bevy_sprite")))] + bevy_log::error!("bevy_gizmos requires either bevy_pbr or bevy_sprite. Please enable one."); + load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); app.register_type::() - .register_type::() .add_plugins(UniformComponentPlugin::::default()) .init_asset::() .add_plugins(RenderAssetPlugin::::default()) .init_resource::() - .init_resource::() - .init_resource::() - .add_systems(Last, update_gizmo_meshes) - .add_systems( - PostUpdate, - ( - draw_aabbs, - draw_all_aabbs.run_if(|config: Res| config.aabb.draw_all), - ) - .after(TransformSystem::TransformPropagate), - ); + // We insert the Resource GizmoConfigStore into the world implicitly here if it does not exist. + .init_gizmo_group::() + .add_plugins(AabbGizmoPlugin); let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app - .add_systems(ExtractSchedule, extract_gizmo_data) - .add_systems( - Render, - prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), - ); + render_app.add_systems( + Render, + prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), + ); #[cfg(feature = "bevy_sprite")] app.add_plugins(pipeline_2d::LineGizmo2dPlugin); @@ -131,152 +140,51 @@ impl Plugin for GizmoPlugin { } } -/// A [`Resource`] that stores configuration for gizmos. -#[derive(Resource, Clone, Reflect)] -#[reflect(Resource)] -pub struct GizmoConfig { - /// Set to `false` to stop drawing gizmos. - /// - /// Defaults to `true`. - pub enabled: bool, - /// Line width specified in pixels. - /// - /// If `line_perspective` is `true` then this is the size in pixels at the camera's near plane. - /// - /// Defaults to `2.0`. - pub line_width: f32, - /// Apply perspective to gizmo lines. - /// - /// This setting only affects 3D, non-orthographic cameras. - /// - /// Defaults to `false`. - pub line_perspective: bool, - /// How closer to the camera than real geometry the line should be. +/// A trait adding `init_gizmo_group()` to the app +pub trait AppGizmoBuilder { + /// Registers [`GizmoConfigGroup`] `T` in the app enabling the use of [Gizmos<T>](crate::gizmos::Gizmos). /// - /// In 2D this setting has no effect and is effectively always -1. - /// - /// Value between -1 and 1 (inclusive). - /// * 0 means that there is no change to the line position when rendering - /// * 1 means it is furthest away from camera as possible - /// * -1 means that it will always render in front of other things. - /// - /// This is typically useful if you are drawing wireframes on top of polygons - /// and your wireframe is z-fighting (flickering on/off) with your main model. - /// You would set this value to a negative number close to 0. - pub depth_bias: f32, - /// Configuration for the [`AabbGizmo`]. - pub aabb: AabbGizmoConfig, - /// Describes which rendering layers gizmos will be rendered to. - /// - /// Gizmos will only be rendered to cameras with intersecting layers. - pub render_layers: RenderLayers, + /// Configurations can be set using the [`GizmoConfigStore`] [`Resource`]. + fn init_gizmo_group(&mut self) -> &mut Self; } -impl Default for GizmoConfig { - fn default() -> Self { - Self { - enabled: true, - line_width: 2., - line_perspective: false, - depth_bias: 0., - aabb: Default::default(), - render_layers: Default::default(), +impl AppGizmoBuilder for App { + fn init_gizmo_group(&mut self) -> &mut Self { + if self.world.contains_resource::>() { + return self; } - } -} - -/// Configuration for drawing the [`Aabb`] component on entities. -#[derive(Clone, Default, Reflect)] -pub struct AabbGizmoConfig { - /// Draws all bounding boxes in the scene when set to `true`. - /// - /// To draw a specific entity's bounding box, you can add the [`AabbGizmo`] component. - /// - /// Defaults to `false`. - pub draw_all: bool, - /// The default color for bounding box gizmos. - /// - /// A random color is chosen per box if `None`. - /// - /// Defaults to `None`. - pub default_color: Option, -} -/// Add this [`Component`] to an entity to draw its [`Aabb`] component. -#[derive(Component, Reflect, Default, Debug)] -#[reflect(Component, Default)] -pub struct AabbGizmo { - /// The color of the box. - /// - /// The default color from the [`GizmoConfig`] resource is used if `None`, - pub color: Option, -} + self.init_resource::>() + .add_systems(Last, update_gizmo_meshes::); -fn draw_aabbs( - query: Query<(Entity, &Aabb, &GlobalTransform, &AabbGizmo)>, - config: Res, - mut gizmos: Gizmos, -) { - for (entity, &aabb, &transform, gizmo) in &query { - let color = gizmo - .color - .or(config.aabb.default_color) - .unwrap_or_else(|| color_from_entity(entity)); - gizmos.cuboid(aabb_transform(aabb, transform), color); - } -} + self.world + .get_resource_or_insert_with::(Default::default) + .register::(); -fn draw_all_aabbs( - query: Query<(Entity, &Aabb, &GlobalTransform), Without>, - config: Res, - mut gizmos: Gizmos, -) { - for (entity, &aabb, &transform) in &query { - let color = config - .aabb - .default_color - .unwrap_or_else(|| color_from_entity(entity)); - gizmos.cuboid(aabb_transform(aabb, transform), color); - } -} - -fn color_from_entity(entity: Entity) -> Color { - let index = entity.index(); + let Ok(render_app) = self.get_sub_app_mut(RenderApp) else { + return self; + }; - // from https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/ - // - // See https://en.wikipedia.org/wiki/Low-discrepancy_sequence - // Map a sequence of integers (eg: 154, 155, 156, 157, 158) into the [0.0..1.0] range, - // so that the closer the numbers are, the larger the difference of their image. - const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up - const RATIO_360: f32 = 360.0 / u32::MAX as f32; - let hue = index.wrapping_mul(FRAC_U32MAX_GOLDEN_RATIO) as f32 * RATIO_360; + render_app.add_systems(ExtractSchedule, extract_gizmo_data::); - Color::hsl(hue, 1., 0.5) -} - -fn aabb_transform(aabb: Aabb, transform: GlobalTransform) -> GlobalTransform { - transform - * GlobalTransform::from( - Transform::from_translation(aabb.center.into()) - .with_scale((aabb.half_extents * 2.).into()), - ) + self + } } #[derive(Resource, Default)] struct LineGizmoHandles { - list: Option>, - strip: Option>, + list: HashMap>, + strip: HashMap>, } -fn update_gizmo_meshes( +fn update_gizmo_meshes( mut line_gizmos: ResMut>, mut handles: ResMut, - mut storage: ResMut, + mut storage: ResMut>, ) { if storage.list_positions.is_empty() { - handles.list = None; - } else if let Some(handle) = handles.list.as_ref() { + handles.list.remove(&TypeId::of::()); + } else if let Some(handle) = handles.list.get(&TypeId::of::()) { let list = line_gizmos.get_mut(handle).unwrap(); list.positions = mem::take(&mut storage.list_positions); @@ -290,12 +198,14 @@ fn update_gizmo_meshes( list.positions = mem::take(&mut storage.list_positions); list.colors = mem::take(&mut storage.list_colors); - handles.list = Some(line_gizmos.add(list)); + handles + .list + .insert(TypeId::of::(), line_gizmos.add(list)); } if storage.strip_positions.is_empty() { - handles.strip = None; - } else if let Some(handle) = handles.strip.as_ref() { + handles.strip.remove(&TypeId::of::()); + } else if let Some(handle) = handles.strip.get(&TypeId::of::()) { let strip = line_gizmos.get_mut(handle).unwrap(); strip.positions = mem::take(&mut storage.strip_positions); @@ -309,24 +219,27 @@ fn update_gizmo_meshes( strip.positions = mem::take(&mut storage.strip_positions); strip.colors = mem::take(&mut storage.strip_colors); - handles.strip = Some(line_gizmos.add(strip)); + handles + .strip + .insert(TypeId::of::(), line_gizmos.add(strip)); } } -fn extract_gizmo_data( +fn extract_gizmo_data( mut commands: Commands, handles: Extract>, - config: Extract>, + config: Extract>, ) { - if config.is_changed() { - commands.insert_resource(config.clone()); - } + let (config, _) = config.config::(); if !config.enabled { return; } - for handle in [&handles.list, &handles.strip].into_iter().flatten() { + for map in [&handles.list, &handles.strip].into_iter() { + let Some(handle) = map.get(&TypeId::of::()) else { + continue; + }; commands.spawn(( LineGizmoUniform { line_width: config.line_width, @@ -334,7 +247,8 @@ fn extract_gizmo_data( #[cfg(feature = "webgl")] _padding: Default::default(), }, - handle.clone_weak(), + (*handle).clone_weak(), + GizmoMeshConfig::from(config), )); } } @@ -365,28 +279,25 @@ struct GpuLineGizmo { } impl RenderAsset for LineGizmo { - type ExtractedAsset = LineGizmo; - type PreparedAsset = GpuLineGizmo; - type Param = SRes; - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() + fn persistence_policy(&self) -> RenderAssetPersistencePolicy { + RenderAssetPersistencePolicy::Keep } fn prepare_asset( - line_gizmo: Self::ExtractedAsset, + self, render_device: &mut SystemParamItem, - ) -> Result> { - let position_buffer_data = cast_slice(&line_gizmo.positions); + ) -> Result> { + let position_buffer_data = cast_slice(&self.positions); let position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, label: Some("LineGizmo Position Buffer"), contents: position_buffer_data, }); - let color_buffer_data = cast_slice(&line_gizmo.colors); + let color_buffer_data = cast_slice(&self.colors); let color_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, label: Some("LineGizmo Color Buffer"), @@ -396,8 +307,8 @@ impl RenderAsset for LineGizmo { Ok(GpuLineGizmo { position_buffer, color_buffer, - vertex_count: line_gizmo.positions.len() as u32, - strip: line_gizmo.strip, + vertex_count: self.positions.len() as u32, + strip: self.strip, }) } } @@ -431,15 +342,15 @@ fn prepare_line_gizmo_bind_group( struct SetLineGizmoBindGroup; impl RenderCommand

for SetLineGizmoBindGroup { - type ViewData = (); - type ItemData = Read>; type Param = SRes; + type ViewQuery = (); + type ItemQuery = Read>; #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewData>, - uniform_index: ROQueryItem<'w, Self::ItemData>, + _view: ROQueryItem<'w, Self::ViewQuery>, + uniform_index: ROQueryItem<'w, Self::ItemQuery>, bind_group: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -454,15 +365,15 @@ impl RenderCommand

for SetLineGizmoBindGroup struct DrawLineGizmo; impl RenderCommand

for DrawLineGizmo { - type ViewData = (); - type ItemData = Read>; type Param = SRes>; + type ViewQuery = (); + type ItemQuery = Read>; #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewData>, - handle: ROQueryItem<'w, Self::ItemData>, + _view: ROQueryItem<'w, Self::ViewQuery>, + handle: ROQueryItem<'w, Self::ItemQuery>, 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 5b64598eb403f..f55453fa67035 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -1,6 +1,6 @@ use crate::{ - line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoConfig, LineGizmo, - LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, + config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoRenderSystem, + LineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; @@ -8,7 +8,7 @@ use bevy_core_pipeline::core_2d::Transparent2d; use bevy_ecs::{ prelude::Entity, - schedule::IntoSystemConfigs, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs}, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; @@ -34,10 +34,14 @@ impl Plugin for LineGizmo2dPlugin { render_app .add_render_command::() .init_resource::>() + .configure_sets( + Render, + GizmoRenderSystem::QueueLineGizmos2d.in_set(RenderSet::Queue), + ) .add_systems( Render, queue_line_gizmos_2d - .in_set(RenderSet::Queue) + .in_set(GizmoRenderSystem::QueueLineGizmos2d) .after(prepare_assets::), ); } @@ -140,8 +144,7 @@ fn queue_line_gizmos_2d( mut pipelines: ResMut>, pipeline_cache: Res, msaa: Res, - config: Res, - line_gizmos: Query<(Entity, &Handle)>, + line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut views: Query<( &ExtractedView, @@ -152,14 +155,15 @@ fn queue_line_gizmos_2d( let draw_function = draw_functions.read().get_id::().unwrap(); for (view, mut transparent_phase, render_layers) in &mut views { - let render_layers = render_layers.copied().unwrap_or_default(); - if !config.render_layers.intersects(&render_layers) { - continue; - } let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); - for (entity, handle) in &line_gizmos { + for (entity, handle, config) in &line_gizmos { + let render_layers = render_layers.copied().unwrap_or_default(); + if !config.render_layers.intersects(&render_layers) { + continue; + } + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue; }; diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 2cb016753513b..bd5064e39d789 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -1,6 +1,6 @@ use crate::{ - line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoConfig, LineGizmo, - LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, + config::GizmoMeshConfig, line_gizmo_vertex_buffer_layouts, DrawLineGizmo, GizmoRenderSystem, + LineGizmo, LineGizmoUniformBindgroupLayout, SetLineGizmoBindGroup, LINE_SHADER_HANDLE, }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; @@ -12,7 +12,7 @@ use bevy_core_pipeline::{ use bevy_ecs::{ prelude::Entity, query::Has, - schedule::IntoSystemConfigs, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs}, system::{Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; @@ -36,10 +36,14 @@ impl Plugin for LineGizmo3dPlugin { render_app .add_render_command::() .init_resource::>() + .configure_sets( + Render, + GizmoRenderSystem::QueueLineGizmos3d.in_set(RenderSet::Queue), + ) .add_systems( Render, queue_line_gizmos_3d - .in_set(RenderSet::Queue) + .in_set(GizmoRenderSystem::QueueLineGizmos3d) .after(prepare_assets::), ); } @@ -155,8 +159,7 @@ fn queue_line_gizmos_3d( mut pipelines: ResMut>, pipeline_cache: Res, msaa: Res, - config: Res, - line_gizmos: Query<(Entity, &Handle)>, + line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut views: Query<( &ExtractedView, @@ -180,9 +183,6 @@ fn queue_line_gizmos_3d( ) in &mut views { let render_layers = render_layers.copied().unwrap_or_default(); - if !config.render_layers.intersects(&render_layers) { - continue; - } let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) | MeshPipelineKey::from_hdr(view.hdr); @@ -203,7 +203,11 @@ fn queue_line_gizmos_3d( view_key |= MeshPipelineKey::DEFERRED_PREPASS; } - for (entity, handle) in &line_gizmos { + for (entity, handle, config) in &line_gizmos { + if !config.render_layers.intersects(&render_layers) { + continue; + } + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue; }; diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 1454fa94e00f2..05cbd9a940e2a 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -35,7 +35,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.12.0" } bevy_utils = { path = "../bevy_utils", version = "0.12.0" } # other -gltf = { version = "1.3.0", default-features = false, features = [ +gltf = { version = "1.4.0", default-features = false, features = [ "KHR_lights_punctual", "KHR_materials_transmission", "KHR_materials_ior", @@ -43,11 +43,12 @@ gltf = { version = "1.3.0", default-features = false, features = [ "KHR_materials_unlit", "KHR_materials_emissive_strength", "extras", + "extensions", "names", "utils", ] } thiserror = "1.0" -base64 = "0.13.0" +base64 = "0.21.5" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index fe47fd4dfdd9f..0e7e882d231f3 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -98,6 +98,8 @@ pub struct Gltf { /// Named animations loaded from the glTF file. #[cfg(feature = "bevy_animation")] pub named_animations: HashMap>, + /// The gltf root of the gltf asset, see . Only has a value when `GltfLoaderSettings::include_source` is true. + pub source: Option, } /// A glTF node with all of its child nodes, its [`GltfMesh`], diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index c4280caf1a08e..9340ef7ebc721 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -22,6 +22,7 @@ use bevy_render::{ }, prelude::SpatialBundle, primitives::Aabb, + render_asset::RenderAssetPersistencePolicy, render_resource::{Face, PrimitiveTopology}, texture::{ CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings, @@ -32,7 +33,7 @@ use bevy_scene::Scene; #[cfg(not(target_arch = "wasm32"))] use bevy_tasks::IoTaskPool; use bevy_transform::components::Transform; -use bevy_utils::{HashMap, HashSet}; +use bevy_utils::{EntityHashMap, HashMap, HashSet}; use gltf::{ accessor::Iter, mesh::{util::ReadIndices, Mode}, @@ -120,7 +121,7 @@ pub struct GltfLoader { /// |s: &mut GltfLoaderSettings| { /// s.load_cameras = false; /// } -/// ); +/// ); /// ``` #[derive(Serialize, Deserialize)] pub struct GltfLoaderSettings { @@ -130,6 +131,8 @@ pub struct GltfLoaderSettings { pub load_cameras: bool, /// If true, the loader will spawn lights for gltf light nodes. pub load_lights: bool, + /// If true, the loader will include the root of the gltf root node. + pub include_source: bool, } impl Default for GltfLoaderSettings { @@ -138,6 +141,7 @@ impl Default for GltfLoaderSettings { load_meshes: true, load_cameras: true, load_lights: true, + include_source: false, } } } @@ -205,7 +209,7 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::Keyframes; + use bevy_animation::{Interpolation, Keyframes}; use gltf::animation::util::ReadOutputs; let mut animations = vec![]; let mut named_animations = HashMap::default(); @@ -213,12 +217,10 @@ async fn load_gltf<'a, 'b, 'c>( for animation in gltf.animations() { let mut animation_clip = bevy_animation::AnimationClip::default(); for channel in animation.channels() { - match channel.sampler().interpolation() { - gltf::animation::Interpolation::Linear => (), - other => warn!( - "Animation interpolation {:?} is not supported, will use linear", - other - ), + let interpolation = match channel.sampler().interpolation() { + gltf::animation::Interpolation::Linear => Interpolation::Linear, + gltf::animation::Interpolation::Step => Interpolation::Step, + gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline, }; let node = channel.target().node(); let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()])); @@ -264,6 +266,7 @@ async fn load_gltf<'a, 'b, 'c>( bevy_animation::VariableCurve { keyframe_timestamps, keyframes, + interpolation, }, ); } else { @@ -390,7 +393,7 @@ async fn load_gltf<'a, 'b, 'c>( let primitive_label = primitive_label(&gltf_mesh, &primitive); let primitive_topology = get_primitive_topology(primitive.mode())?; - let mut mesh = Mesh::new(primitive_topology); + let mut mesh = Mesh::new(primitive_topology, RenderAssetPersistencePolicy::Keep); // Read vertex attributes for (semantic, accessor) in primitive.attributes() { @@ -434,6 +437,7 @@ async fn load_gltf<'a, 'b, 'c>( let morph_target_image = MorphTargetImage::new( morph_target_reader.map(PrimitiveMorphAttributesIter), mesh.count_vertices(), + RenderAssetPersistencePolicy::Keep, )?; let handle = load_context.add_labeled_asset(morph_targets_label, morph_target_image.0); @@ -521,20 +525,7 @@ async fn load_gltf<'a, 'b, 'c>( .mesh() .map(|mesh| mesh.index()) .and_then(|i| meshes.get(i).cloned()), - transform: match node.transform() { - gltf::scene::Transform::Matrix { matrix } => { - Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) - } - gltf::scene::Transform::Decomposed { - translation, - rotation, - scale, - } => Transform { - translation: bevy_math::Vec3::from(translation), - rotation: bevy_math::Quat::from_array(rotation), - scale: bevy_math::Vec3::from(scale), - }, - }, + transform: node_transform(&node), extras: get_gltf_extras(node.extras()), }, node.children() @@ -582,7 +573,7 @@ async fn load_gltf<'a, 'b, 'c>( let mut err = None; let mut world = World::default(); let mut node_index_to_entity_map = HashMap::new(); - let mut entity_to_skin_index_map = HashMap::new(); + let mut entity_to_skin_index_map = EntityHashMap::default(); let mut scene_load_context = load_context.begin_labeled_asset(); world .spawn(SpatialBundle::INHERITED_IDENTITY) @@ -672,6 +663,11 @@ async fn load_gltf<'a, 'b, 'c>( animations, #[cfg(feature = "bevy_animation")] named_animations, + source: if settings.include_source { + Some(gltf) + } else { + None + }, }) } @@ -681,6 +677,29 @@ fn get_gltf_extras(extras: &gltf::json::Extras) -> Option { }) } +/// Calculate the transform of gLTF node. +/// +/// This should be used instead of calling [`gltf::scene::Transform::matrix()`] +/// on [`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. +fn node_transform(node: &Node) -> Transform { + match node.transform() { + gltf::scene::Transform::Matrix { matrix } => { + Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) + } + gltf::scene::Transform::Decomposed { + translation, + rotation, + scale, + } => Transform { + translation: bevy_math::Vec3::from(translation), + rotation: bevy_math::Quat::from_array(rotation), + scale: bevy_math::Vec3::from(scale), + }, + } +} + fn node_name(node: &Node) -> Name { let name = node .name() @@ -725,6 +744,7 @@ async fn load_image<'a, 'b>( supported_compressed_formats, is_srgb, ImageSampler::Descriptor(sampler_descriptor), + RenderAssetPersistencePolicy::Keep, )?; Ok(ImageOrPath::Image { image, @@ -746,6 +766,7 @@ async fn load_image<'a, 'b>( supported_compressed_formats, is_srgb, ImageSampler::Descriptor(sampler_descriptor), + RenderAssetPersistencePolicy::Keep, )?, label: texture_label(&gltf_texture), }) @@ -893,7 +914,7 @@ fn load_material( } /// Loads a glTF node. -#[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments, clippy::result_large_err)] fn load_node( gltf_node: &Node, world_builder: &mut WorldChildBuilder, @@ -901,13 +922,12 @@ fn load_node( load_context: &mut LoadContext, settings: &GltfLoaderSettings, node_index_to_entity_map: &mut HashMap, - entity_to_skin_index_map: &mut HashMap, + entity_to_skin_index_map: &mut EntityHashMap, active_camera_found: &mut bool, parent_transform: &Transform, ) -> Result<(), GltfError> { - let transform = gltf_node.transform(); let mut gltf_error = None; - let transform = Transform::from_matrix(Mat4::from_cols_array_2d(&transform.matrix())); + let transform = node_transform(gltf_node); 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 @@ -1291,6 +1311,7 @@ fn texture_address_mode(gltf_address_mode: &WrappingMode) -> ImageAddressMode { } /// Maps the `primitive_topology` form glTF to `wgpu`. +#[allow(clippy::result_large_err)] fn get_primitive_topology(mode: Mode) -> Result { match mode { Mode::Points => Ok(PrimitiveTopology::PointList), @@ -1448,7 +1469,7 @@ impl<'a> DataUri<'a> { fn decode(&self) -> Result, base64::DecodeError> { if self.base64 { - base64::decode(self.data) + base64::Engine::decode(&base64::engine::general_purpose::STANDARD, self.data) } else { Ok(self.data.as_bytes().to_owned()) } diff --git a/crates/bevy_hierarchy/Cargo.toml b/crates/bevy_hierarchy/Cargo.toml index 47d492f3118c2..202c3302fb426 100644 --- a/crates/bevy_hierarchy/Cargo.toml +++ b/crates/bevy_hierarchy/Cargo.toml @@ -25,8 +25,5 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.12.0", features = [ ], optional = true } bevy_utils = { path = "../bevy_utils", version = "0.12.0" } -# other -smallvec = { version = "1.6", features = ["serde", "union", "const_generics"] } - [lints] workspace = true diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 801620430ad58..d80c44c29e5e9 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -6,7 +6,7 @@ use bevy_ecs::{ system::{Command, Commands, EntityCommands}, world::{EntityWorldMut, World}, }; -use smallvec::SmallVec; +use bevy_utils::smallvec::{smallvec, SmallVec}; // Do not use `world.send_event_batch` as it prints error message when the Events are not available in the world, // even though it's a valid use case to execute commands on a world without events. Loading a GLTF file for example @@ -24,7 +24,7 @@ fn push_child_unchecked(world: &mut World, parent: Entity, child: Entity) { if let Some(mut children) = parent.get_mut::() { children.0.push(child); } else { - parent.insert(Children(smallvec::smallvec![child])); + parent.insert(Children(smallvec![child])); } } @@ -161,14 +161,14 @@ fn clear_children(parent: Entity, world: &mut World) { /// Command that adds a child to an entity. #[derive(Debug)] -pub struct AddChild { +pub struct PushChild { /// Parent entity to add the child to. pub parent: Entity, /// Child entity to add. pub child: Entity, } -impl Command for AddChild { +impl Command for PushChild { fn apply(self, world: &mut World) { world.entity_mut(self.parent).add_child(self.child); } @@ -433,7 +433,7 @@ impl<'w, 's, 'a> BuildChildren for EntityCommands<'w, 's, 'a> { if child == parent { panic!("Cannot add entity as a child of itself."); } - self.commands().add(AddChild { child, parent }); + self.commands().add(PushChild { child, parent }); self } @@ -460,7 +460,7 @@ impl<'w, 's, 'a> BuildChildren for EntityCommands<'w, 's, 'a> { if child == parent { panic!("Cannot set parent to itself"); } - self.commands().add(AddChild { child, parent }); + self.commands().add(PushChild { child, parent }); self } @@ -696,7 +696,7 @@ mod tests { components::{Children, Parent}, HierarchyEvent::{self, ChildAdded, ChildMoved, ChildRemoved}, }; - use smallvec::{smallvec, SmallVec}; + use bevy_utils::smallvec::{smallvec, SmallVec}; use bevy_ecs::{ component::Component, diff --git a/crates/bevy_hierarchy/src/components/children.rs b/crates/bevy_hierarchy/src/components/children.rs index b2186a1251a79..57556747fcef5 100644 --- a/crates/bevy_hierarchy/src/components/children.rs +++ b/crates/bevy_hierarchy/src/components/children.rs @@ -6,8 +6,8 @@ use bevy_ecs::{ prelude::FromWorld, world::World, }; +use bevy_utils::smallvec::SmallVec; use core::slice; -use smallvec::SmallVec; use std::ops::Deref; /// Contains references to the child entities of this entity. @@ -41,6 +41,7 @@ impl MapEntities for Children { // However Children should only ever be set with a real user-defined entities. Its worth looking // into better ways to handle cases like this. impl FromWorld for Children { + #[inline] fn from_world(_world: &mut World) -> Self { Children(SmallVec::new()) } @@ -48,11 +49,13 @@ impl FromWorld for Children { impl Children { /// Constructs a [`Children`] component with the given entities. + #[inline] pub(crate) fn from_entities(entities: &[Entity]) -> Self { Self(SmallVec::from_slice(entities)) } /// Swaps the child at `a_index` with the child at `b_index`. + #[inline] pub fn swap(&mut self, a_index: usize, b_index: usize) { self.0.swap(a_index, b_index); } @@ -65,6 +68,7 @@ impl Children { /// For the unstable version, see [`sort_unstable_by`](Children::sort_unstable_by). /// /// See also [`sort_by_key`](Children::sort_by_key), [`sort_by_cached_key`](Children::sort_by_cached_key). + #[inline] pub fn sort_by(&mut self, compare: F) where F: FnMut(&Entity, &Entity) -> std::cmp::Ordering, @@ -80,6 +84,7 @@ impl Children { /// For the unstable version, see [`sort_unstable_by_key`](Children::sort_unstable_by_key). /// /// See also [`sort_by`](Children::sort_by), [`sort_by_cached_key`](Children::sort_by_cached_key). + #[inline] pub fn sort_by_key(&mut self, compare: F) where F: FnMut(&Entity) -> K, @@ -95,6 +100,7 @@ impl Children { /// For the underlying implementation, see [`slice::sort_by_cached_key`]. /// /// See also [`sort_by`](Children::sort_by), [`sort_by_key`](Children::sort_by_key). + #[inline] pub fn sort_by_cached_key(&mut self, compare: F) where F: FnMut(&Entity) -> K, @@ -111,6 +117,7 @@ impl Children { /// For the stable version, see [`sort_by`](Children::sort_by). /// /// See also [`sort_unstable_by_key`](Children::sort_unstable_by_key). + #[inline] pub fn sort_unstable_by(&mut self, compare: F) where F: FnMut(&Entity, &Entity) -> std::cmp::Ordering, @@ -126,6 +133,7 @@ impl Children { /// For the stable version, see [`sort_by_key`](Children::sort_by_key). /// /// See also [`sort_unstable_by`](Children::sort_unstable_by). + #[inline] pub fn sort_unstable_by_key(&mut self, compare: F) where F: FnMut(&Entity) -> K, @@ -138,6 +146,7 @@ impl Children { impl Deref for Children { type Target = [Entity]; + #[inline(always)] fn deref(&self) -> &Self::Target { &self.0[..] } @@ -148,6 +157,7 @@ impl<'a> IntoIterator for &'a Children { type IntoIter = slice::Iter<'a, Entity>; + #[inline(always)] fn into_iter(self) -> Self::IntoIter { self.0.iter() } diff --git a/crates/bevy_hierarchy/src/components/parent.rs b/crates/bevy_hierarchy/src/components/parent.rs index bffd3c28f3646..2ba6a10ccb779 100644 --- a/crates/bevy_hierarchy/src/components/parent.rs +++ b/crates/bevy_hierarchy/src/components/parent.rs @@ -27,6 +27,7 @@ pub struct Parent(pub(crate) Entity); impl Parent { /// Gets the [`Entity`] ID of the parent. + #[inline(always)] pub fn get(&self) -> Entity { self.0 } @@ -37,6 +38,7 @@ impl Parent { /// for both [`Children`] & [`Parent`] that is agnostic to edge direction. /// /// [`Children`]: super::children::Children + #[inline(always)] pub fn as_slice(&self) -> &[Entity] { std::slice::from_ref(&self.0) } @@ -47,6 +49,7 @@ impl Parent { // However Parent should only ever be set with a real user-defined entity. Its worth looking into // better ways to handle cases like this. impl FromWorld for Parent { + #[inline(always)] fn from_world(_world: &mut World) -> Self { Parent(Entity::PLACEHOLDER) } @@ -61,6 +64,7 @@ impl MapEntities for Parent { impl Deref for Parent { type Target = Entity; + #[inline(always)] fn deref(&self) -> &Self::Target { &self.0 } diff --git a/crates/bevy_hierarchy/src/lib.rs b/crates/bevy_hierarchy/src/lib.rs index 15d9d2fa425d6..da4b33f341bd3 100644 --- a/crates/bevy_hierarchy/src/lib.rs +++ b/crates/bevy_hierarchy/src/lib.rs @@ -1,8 +1,50 @@ #![warn(missing_docs)] -//! `bevy_hierarchy` can be used to define hierarchies of entities. +//! Parent-child relationships for Bevy entities. //! -//! Most commonly, these hierarchies are used for inheriting `Transform` values -//! from the [`Parent`] to its [`Children`]. +//! You should use the tools in this crate +//! whenever you want to organize your entities in a hierarchical fashion, +//! to make groups of entities more manageable, +//! or to propagate properties throughout the entity hierarchy. +//! +//! This crate introduces various tools, including a [plugin] +//! for managing parent-child relationships between entities. +//! It provides two components, [`Parent`] and [`Children`], +//! to store references to related entities. +//! It also provides [command] and [world] API extensions +//! to set and clear those relationships. +//! +//! More advanced users may also appreciate +//! [query extension methods] to traverse hierarchies, +//! and [events] to notify hierarchical changes. +//! There is also a [diagnostic plugin] to validate property propagation. +//! +//! # Hierarchy management +//! +//! The methods defined in this crate fully manage +//! the components responsible for defining the entity hierarchy. +//! Mutating these components manually may result in hierarchy invalidation. +//! +//! Hierarchical relationships are always managed symmetrically. +//! For example, assigning a child to an entity +//! will always set the parent in the other, +//! and vice versa. +//! Similarly, unassigning a child in the parent +//! will always unassign the parent in the child. +//! +//! ## Despawning entities +//! +//! The commands and methods provided by `bevy_ecs` to despawn entities +//! are not capable of automatically despawning hierarchies of entities. +//! In most cases, these operations will invalidate the hierarchy. +//! Instead, you should use the provided [hierarchical despawn extension methods]. +//! +//! [command]: BuildChildren +//! [diagnostic plugin]: ValidParentCheckPlugin +//! [events]: HierarchyEvent +//! [hierarchical despawn extension methods]: DespawnRecursiveExt +//! [plugin]: HierarchyPlugin +//! [query extension methods]: HierarchyQueryExt +//! [world]: BuildWorldChildren mod components; pub use components::*; @@ -35,16 +77,21 @@ pub mod prelude { #[cfg(feature = "bevy_app")] use bevy_app::prelude::*; -/// The base plugin for handling [`Parent`] and [`Children`] components +/// Provides hierarchy functionality to a Bevy app. +/// +/// Check the [crate-level documentation] for all the features. +/// +/// [crate-level documentation]: crate #[derive(Default)] pub struct HierarchyPlugin; #[cfg(feature = "bevy_app")] +use bevy_utils::smallvec::SmallVec; impl Plugin for HierarchyPlugin { fn build(&self, app: &mut App) { app.register_type::() .register_type::() - .register_type::>() + .register_type::>() .add_event::(); } } diff --git a/crates/bevy_input/src/common_conditions.rs b/crates/bevy_input/src/common_conditions.rs index 502ded76e8734..06e0b96375f4f 100644 --- a/crates/bevy_input/src/common_conditions.rs +++ b/crates/bevy_input/src/common_conditions.rs @@ -4,7 +4,7 @@ use std::hash::Hash; /// Stateful run condition that can be toggled via a input press using [`ButtonInput::just_pressed`]. /// -/// ```rust,no_run +/// ```no_run /// use bevy::prelude::*; /// use bevy::input::common_conditions::input_toggle_active; /// @@ -22,7 +22,7 @@ use std::hash::Hash; /// /// If you want other systems to be able to access whether the toggled state is active, /// you should use a custom resource or a state for that: -/// ```rust,no_run +/// ```no_run /// use bevy::prelude::*; /// use bevy::input::common_conditions::input_just_pressed; /// @@ -71,7 +71,7 @@ where /// Run condition that is active if [`ButtonInput::just_pressed`] is true for the given input. /// -/// ```rust,no_run +/// ```no_run /// use bevy::prelude::*; /// use bevy::input::common_conditions::input_just_pressed; /// fn main() { diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index 383a750a5e159..8b7b784db259c 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -1,5 +1,70 @@ //! The keyboard input functionality. +// This file contains a substantial portion of the UI Events Specification by the W3C. In +// particular, the variant names within `KeyCode` and their documentation are modified +// versions of contents of the aforementioned specification. +// +// The original documents are: +// +// +// ### For `KeyCode` +// UI Events KeyboardEvent code Values +// https://www.w3.org/TR/2017/CR-uievents-code-20170601/ +// Copyright © 2017 W3C® (MIT, ERCIM, Keio, Beihang). +// +// These documents were used under the terms of the following license. This W3C license as well as +// the W3C short notice apply to the `KeyCode` enums and their variants and the +// documentation attached to their variants. + +// --------- BEGGINING OF W3C LICENSE -------------------------------------------------------------- +// +// License +// +// By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, +// and will comply with the following terms and conditions. +// +// Permission to copy, modify, and distribute this work, with or without modification, for any +// purpose and without fee or royalty is hereby granted, provided that you include the following on +// ALL copies of the work or portions thereof, including modifications: +// +// - The full text of this NOTICE in a location viewable to users of the redistributed or derivative +// work. +// - Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none +// exist, the W3C Software and Document Short Notice should be included. +// - Notice of any changes or modifications, through a copyright statement on the new code or +// document such as "This software or document includes material copied from or derived from +// [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +// +// Disclaimers +// +// THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR +// ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD +// PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +// +// COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES +// ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +// +// The name and trademarks of copyright holders may NOT be used in advertising or publicity +// pertaining to the work without specific, written prior permission. Title to copyright in this +// work will at all times remain with copyright holders. +// +// --------- END OF W3C LICENSE -------------------------------------------------------------------- + +// --------- BEGGINING OF W3C SHORT NOTICE --------------------------------------------------------- +// +// winit: https://github.com/rust-windowing/winit +// +// Copyright © 2021 World Wide Web Consortium, (Massachusetts Institute of Technology, European +// Research Consortium for Informatics and Mathematics, Keio University, Beihang). All Rights +// Reserved. This work is distributed under the W3C® Software License [1] in the hope that it will +// be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// [1] http://www.w3.org/Consortium/Legal/copyright-software +// +// --------- END OF W3C SHORT NOTICE --------------------------------------------------------------- + use crate::{ButtonInput, ButtonState}; use bevy_ecs::entity::Entity; use bevy_ecs::{ @@ -29,10 +94,8 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; reflect(Serialize, Deserialize) )] pub struct KeyboardInput { - /// The scan code of the key. - pub scan_code: u32, /// The key code of the key. - pub key_code: Option, + pub key_code: KeyCode, /// The press state of the key. pub state: ButtonState, /// Window that received the input. @@ -43,39 +106,69 @@ pub struct KeyboardInput { /// /// ## Differences /// -/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] or [`ButtonInput`] resources is that +/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources is that /// the latter have convenient functions such as [`ButtonInput::pressed`], [`ButtonInput::just_pressed`] and [`ButtonInput::just_released`]. pub fn keyboard_input_system( - mut scan_input: ResMut>, mut key_input: ResMut>, mut keyboard_input_events: EventReader, ) { // Avoid clearing if it's not empty to ensure change detection is not triggered. - scan_input.bypass_change_detection().clear(); key_input.bypass_change_detection().clear(); for event in keyboard_input_events.read() { let KeyboardInput { - scan_code, state, .. + key_code, state, .. } = event; - if let Some(key_code) = event.key_code { - match state { - ButtonState::Pressed => key_input.press(key_code), - ButtonState::Released => key_input.release(key_code), - } - } match state { - ButtonState::Pressed => scan_input.press(ScanCode(*scan_code)), - ButtonState::Released => scan_input.release(ScanCode(*scan_code)), + ButtonState::Pressed => key_input.press(*key_code), + ButtonState::Released => key_input.release(*key_code), } } } +/// Contains the platform-native physical key identifier +/// +/// The exact values vary from platform to platform (which is part of why this is a per-platform +/// enum), but the values are primarily tied to the key's physical location on the keyboard. +/// +/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native +/// physical key identifier to a meaningful [`KeyCode`] variant. In the presence of identifiers we +/// haven't mapped for you yet, this lets you use use [`KeyCode`] to: +/// +/// - Correctly match key press and release events. +/// - On non-web platforms, support assigning keybinds to virtually any key through a UI. +#[derive(Debug, Clone, Ord, PartialOrd, Copy, PartialEq, Eq, Hash, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum NativeKeyCode { + /// Unidentified + Unidentified, + /// An Android "scancode". + Android(u32), + /// A macOS "scancode". + MacOS(u16), + /// A Windows "scancode". + Windows(u16), + /// An XKB "keycode". + Xkb(u32), +} + /// The key code of a [`KeyboardInput`]. /// /// ## Usage /// /// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. -/// The resource values are mapped to the current layout of the keyboard and correlate to an [`ScanCode`]. +/// +/// Code representing the location of a physical key +/// 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 /// /// ## Updating /// @@ -89,375 +182,471 @@ pub fn keyboard_input_system( )] #[repr(u32)] pub enum KeyCode { - /// The `1` key over the letters. - Key1, - /// The `2` key over the letters. - Key2, - /// The `3` key over the letters. - Key3, - /// The `4` key over the letters. - Key4, - /// The `5` key over the letters. - Key5, - /// The `6` key over the letters. - Key6, - /// The `7` key over the letters. - Key7, - /// The `8` key over the letters. - Key8, - /// The `9` key over the letters. - Key9, - /// The `0` key over the letters. - Key0, - - /// The `A` key. - A, - /// The `B` key. - B, - /// The `C` key. - C, - /// The `D` key. - D, - /// The `E` key. - E, - /// The `F` key. - F, - /// The `G` key. - G, - /// The `H` key. - H, - /// The `I` key. - I, - /// The `J` key. - J, - /// The `K` key. - K, - /// The `L` key. - L, - /// The `M` key. - M, - /// The `N` key. - N, - /// The `O` key. - O, - /// The `P` key. - P, - /// The `Q` key. - Q, - /// The `R` key. - R, - /// The `S` key. - S, - /// The `T` key. - T, - /// The `U` key. - U, - /// The `V` key. - V, - /// The `W` key. - W, - /// The `X` key. - X, - /// The `Y` key. - Y, - /// The `Z` key. - Z, - - /// The `Escape` / `ESC` key, next to the `F1` key. + /// This variant is used when the key cannot be translated to any other variant. + /// + /// The native keycode is provided (if available) so you're able to more reliably match + /// key-press and key-release events by hashing the [`KeyCode`]. It is also possible to use + /// this for keybinds for non-standard keys, but such keybinds are tied to a given platform. + Unidentified(NativeKeyCode), + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or . + AltLeft, + /// Alt, Option, or . + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or . + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or + ControlLeft, + /// Control or + ControlRight, + /// Enter or . Labeled Return on Apple keyboards. + Enter, + /// The Windows, , Command, or other OS symbol key. + SuperLeft, + /// The Windows, , Command, or other OS symbol key. + SuperRight, + /// Shift or + ShiftLeft, + /// Shift or + ShiftRight, + ///   (space) + Space, + /// Tab or + Tab, + /// Japanese: (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): (kana) + Lang1, + /// Korean: Hanja (hanja) + /// + /// Japanese (Mac keyboard): (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// . The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or + PageDown, + /// Page Up, PgUp, or + PageUp, + /// + ArrowDown, + /// + ArrowLeft, + /// + ArrowRight, + /// + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + /// The Enter key on the numpad. + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or Escape, - - /// The `F1` key. + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + /// BrowserFavorites + BrowserFavorites, + /// Some laptops place this key to the right of the key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + /// BrowserRefresh + BrowserRefresh, + /// BrowserSearch + BrowserSearch, + /// BrowserStop + BrowserStop, + /// Eject or . This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + /// LaunchMail + LaunchMail, + /// MediaPlayPause + MediaPlayPause, + /// MediaSelect + MediaSelect, + /// MediaStop + MediaStop, + /// MediaTrackNext + MediaTrackNext, + /// MediaTrackPrevious + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject key. + Power, + /// Sleep + Sleep, + /// AudioVolumeDown + AudioVolumeDown, + /// AudioVolumeMute + AudioVolumeMute, + /// AudioVolumeUp + AudioVolumeUp, + /// WakeUp + WakeUp, + /// Legacy modifier key. Also called "Super" in certain places. + Meta, + /// Legacy modifier key. + Hyper, + /// Turbo + Turbo, + /// Abort + Abort, + /// Resume + Resume, + /// Suspend + Suspend, + /// Found on Sun’s USB keyboard. + Again, + /// Found on Sun’s USB keyboard. + Copy, + /// Found on Sun’s USB keyboard. + Cut, + /// Found on Sun’s USB keyboard. + Find, + /// Found on Sun’s USB keyboard. + Open, + /// Found on Sun’s USB keyboard. + Paste, + /// Found on Sun’s USB keyboard. + Props, + /// Found on Sun’s USB keyboard. + Select, + /// Found on Sun’s USB keyboard. + Undo, + /// Use for dedicated ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ key found on some Japanese word processing keyboards. + Katakana, + /// General-purpose function key. + /// Usually found at the top of the keyboard. F1, - /// The `F2` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F2, - /// The `F3` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F3, - /// The `F4` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F4, - /// The `F5` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F5, - /// The `F6` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F6, - /// The `F7` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F7, - /// The `F8` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F8, - /// The `F9` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F9, - /// The `F10` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F10, - /// The `F11` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F11, - /// The `F12` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F12, - /// The `F13` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F13, - /// The `F14` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F14, - /// The `F15` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F15, - /// The `F16` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F16, - /// The `F17` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F17, - /// The `F18` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F18, - /// The `F19` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F19, - /// The `F20` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F20, - /// The `F21` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F21, - /// The `F22` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F22, - /// The `F23` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F23, - /// The `F24` key. + /// General-purpose function key. + /// Usually found at the top of the keyboard. F24, - - /// The `Snapshot` / `Print Screen` key. - Snapshot, - /// The `Scroll` / `Scroll Lock` key. - Scroll, - /// The `Pause` / `Break` key, next to the `Scroll` key. - Pause, - - /// The `Insert` key, next to the `Backspace` key. - Insert, - /// The `Home` key. - Home, - /// The `Delete` key. - Delete, - /// The `End` key. - End, - /// The `PageDown` key. - PageDown, - /// The `PageUp` key. - PageUp, - - /// The `Left` / `Left Arrow` key. - Left, - /// The `Up` / `Up Arrow` key. - Up, - /// The `Right` / `Right Arrow` key. - Right, - /// The `Down` / `Down Arrow` key. - Down, - - /// The `Back` / `Backspace` key. - Back, - /// The `Return` / `Enter` key. - Return, - /// The `Space` / `Spacebar` / ` ` key. - Space, - - /// The `Compose` key on Linux. - Compose, - /// The `Caret` / `^` key. - Caret, - - /// The `Numlock` key. - Numlock, - /// The `Numpad0` / `0` key. - Numpad0, - /// The `Numpad1` / `1` key. - Numpad1, - /// The `Numpad2` / `2` key. - Numpad2, - /// The `Numpad3` / `3` key. - Numpad3, - /// The `Numpad4` / `4` key. - Numpad4, - /// The `Numpad5` / `5` key. - Numpad5, - /// The `Numpad6` / `6` key. - Numpad6, - /// The `Numpad7` / `7` key. - Numpad7, - /// The `Numpad8` / `8` key. - Numpad8, - /// The `Numpad9` / `9` key. - Numpad9, - - /// The `AbntC1` key. - AbntC1, - /// The `AbntC2` key. - AbntC2, - - /// The `NumpadAdd` / `+` key. - NumpadAdd, - /// The `Apostrophe` / `'` key. - Apostrophe, - /// The `Apps` key. - Apps, - /// The `Asterisk` / `*` key. - Asterisk, - /// The `Plus` / `+` key. - Plus, - /// The `At` / `@` key. - At, - /// The `Ax` key. - Ax, - /// The `Backslash` / `\` key. - Backslash, - /// The `Calculator` key. - Calculator, - /// The `Capital` key. - Capital, - /// The `Colon` / `:` key. - Colon, - /// The `Comma` / `,` key. - Comma, - /// The `Convert` key. - Convert, - /// The `NumpadDecimal` / `.` key. - NumpadDecimal, - /// The `NumpadDivide` / `/` key. - NumpadDivide, - /// The `Equals` / `=` key. - Equals, - /// The `Grave` / `Backtick` / `` ` `` key. - Grave, - /// The `Kana` key. - Kana, - /// The `Kanji` key. - Kanji, - - /// The `Left Alt` key. Maps to `Left Option` on Mac. - AltLeft, - /// The `Left Bracket` / `[` key. - BracketLeft, - /// The `Left Control` key. - ControlLeft, - /// The `Left Shift` key. - ShiftLeft, - /// The `Left Super` key. - /// Generic keyboards usually display this key with the *Microsoft Windows* logo. - /// Apple keyboards call this key the *Command Key* and display it using the ⌘ character. - #[doc(alias("LWin", "LMeta", "LLogo"))] - SuperLeft, - - /// The `Mail` key. - Mail, - /// The `MediaSelect` key. - MediaSelect, - /// The `MediaStop` key. - MediaStop, - /// The `Minus` / `-` key. - Minus, - /// The `NumpadMultiply` / `*` key. - NumpadMultiply, - /// The `Mute` key. - Mute, - /// The `MyComputer` key. - MyComputer, - /// The `NavigateForward` / `Prior` key. - NavigateForward, - /// The `NavigateBackward` / `Next` key. - NavigateBackward, - /// The `NextTrack` key. - NextTrack, - /// The `NoConvert` key. - NoConvert, - /// The `NumpadComma` / `,` key. - NumpadComma, - /// The `NumpadEnter` key. - NumpadEnter, - /// The `NumpadEquals` / `=` key. - NumpadEquals, - /// The `Oem102` key. - Oem102, - /// The `Period` / `.` key. - Period, - /// The `PlayPause` key. - PlayPause, - /// The `Power` key. - Power, - /// The `PrevTrack` key. - PrevTrack, - - /// The `Right Alt` key. Maps to `Right Option` on Mac. - AltRight, - /// The `Right Bracket` / `]` key. - BracketRight, - /// The `Right Control` key. - ControlRight, - /// The `Right Shift` key. - ShiftRight, - /// The `Right Super` key. - /// Generic keyboards usually display this key with the *Microsoft Windows* logo. - /// Apple keyboards call this key the *Command Key* and display it using the ⌘ character. - #[doc(alias("RWin", "RMeta", "RLogo"))] - SuperRight, - - /// The `Semicolon` / `;` key. - Semicolon, - /// The `Slash` / `/` key. - Slash, - /// The `Sleep` key. - Sleep, - /// The `Stop` key. - Stop, - /// The `NumpadSubtract` / `-` key. - NumpadSubtract, - /// The `Sysrq` key. - Sysrq, - /// The `Tab` / ` ` key. - Tab, - /// The `Underline` / `_` key. - Underline, - /// The `Unlabeled` key. - Unlabeled, - - /// The `VolumeDown` key. - VolumeDown, - /// The `VolumeUp` key. - VolumeUp, - - /// The `Wake` key. - Wake, - - /// The `WebBack` key. - WebBack, - /// The `WebFavorites` key. - WebFavorites, - /// The `WebForward` key. - WebForward, - /// The `WebHome` key. - WebHome, - /// The `WebRefresh` key. - WebRefresh, - /// The `WebSearch` key. - WebSearch, - /// The `WebStop` key. - WebStop, - - /// The `Yen` key. - Yen, - - /// The `Copy` key. - Copy, - /// The `Paste` key. - Paste, - /// The `Cut` key. - Cut, + /// General-purpose function key. + F25, + /// General-purpose function key. + F26, + /// General-purpose function key. + F27, + /// General-purpose function key. + F28, + /// General-purpose function key. + F29, + /// General-purpose function key. + F30, + /// General-purpose function key. + F31, + /// General-purpose function key. + F32, + /// General-purpose function key. + F33, + /// General-purpose function key. + F34, + /// General-purpose function key. + F35, } - -/// The scan code of a [`KeyboardInput`]. -/// -/// ## Usage -/// -/// It is used as the generic `` value of an [`ButtonInput`] to create a `Res>`. -/// The resource values are mapped to the physical location of a key on the keyboard and correlate to an [`KeyCode`] -/// -/// ## Updating -/// -/// The resource is updated inside of the [`keyboard_input_system`]. -#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy, Reflect)] -#[reflect(Debug, Hash, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub struct ScanCode(pub u32); diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index ae22786f30656..c3f2b3af9e4f2 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -26,7 +26,7 @@ pub mod prelude { gamepad::{ Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, Gamepads, }, - keyboard::{KeyCode, ScanCode}, + keyboard::KeyCode, mouse::MouseButton, touch::{TouchInput, Touches}, Axis, ButtonInput, @@ -36,7 +36,7 @@ pub mod prelude { use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::Reflect; -use keyboard::{keyboard_input_system, KeyCode, KeyboardInput, ScanCode}; +use keyboard::{keyboard_input_system, KeyCode, KeyboardInput}; use mouse::{ mouse_button_input_system, MouseButton, MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel, @@ -69,7 +69,6 @@ impl Plugin for InputPlugin { // keyboard .add_event::() .init_resource::>() - .init_resource::>() .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystem)) // mouse .add_event::() @@ -115,8 +114,7 @@ impl Plugin for InputPlugin { // Register keyboard types app.register_type::() - .register_type::() - .register_type::(); + .register_type::(); // Register mouse types app.register_type::() diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index 7c90ff1fcb2c6..4eb34c6187032 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -61,6 +61,10 @@ pub enum MouseButton { Right, /// The middle mouse button. Middle, + /// The back mouse button. + Back, + /// The forward mouse button. + Forward, /// Another mouse button with the associated number. Other(u16), } diff --git a/crates/bevy_input/src/touch.rs b/crates/bevy_input/src/touch.rs index be8235fec0253..9b6a61b2bcd4e 100644 --- a/crates/bevy_input/src/touch.rs +++ b/crates/bevy_input/src/touch.rs @@ -1,5 +1,6 @@ //! The touch input functionality. +use bevy_ecs::entity::Entity; use bevy_ecs::event::{Event, EventReader}; use bevy_ecs::system::{ResMut, Resource}; use bevy_math::Vec2; @@ -44,6 +45,8 @@ pub struct TouchInput { pub phase: TouchPhase, /// The position of the finger on the touchscreen. pub position: Vec2, + /// The window entity registering the touch. + pub window: Entity, /// Describes how hard the screen was pressed. /// /// May be [`None`] if the platform does not support pressure sensitivity. @@ -252,11 +255,30 @@ impl Touches { !self.just_pressed.is_empty() } + /// Register a release for a given touch input. + pub fn release(&mut self, id: u64) { + if let Some(touch) = self.pressed.remove(&id) { + self.just_released.insert(id, touch); + } + } + + /// Registers a release for all currently pressed touch inputs. + pub fn release_all(&mut self) { + self.just_released.extend(self.pressed.drain()); + } + /// Returns `true` if the input corresponding to the `id` has just been pressed. pub fn just_pressed(&self, id: u64) -> bool { self.just_pressed.contains_key(&id) } + /// Clears the `just_pressed` state of the touch input and returns `true` if the touch input has just been pressed. + /// + /// Future calls to [`Touches::just_pressed`] for the given touch input will return false until a new press event occurs. + pub fn clear_just_pressed(&mut self, id: u64) -> bool { + self.just_pressed.remove(&id).is_some() + } + /// An iterator visiting every just pressed [`Touch`] input in arbitrary order. pub fn iter_just_pressed(&self) -> impl Iterator { self.just_pressed.values() @@ -277,6 +299,13 @@ impl Touches { self.just_released.contains_key(&id) } + /// Clears the `just_released` state of the touch input and returns `true` if the touch input has just been released. + /// + /// Future calls to [`Touches::just_released`] for the given touch input will return false until a new release event occurs. + pub fn clear_just_released(&mut self, id: u64) -> bool { + self.just_released.remove(&id).is_some() + } + /// An iterator visiting every just released [`Touch`] input in arbitrary order. pub fn iter_just_released(&self) -> impl Iterator { self.just_released.values() @@ -292,6 +321,13 @@ impl Touches { self.just_canceled.contains_key(&id) } + /// Clears the `just_canceled` state of the touch input and returns `true` if the touch input has just been canceled. + /// + /// Future calls to [`Touches::just_canceled`] for the given touch input will return false until a new cancel event occurs. + pub fn clear_just_canceled(&mut self, id: u64) -> bool { + self.just_canceled.remove(&id).is_some() + } + /// An iterator visiting every just canceled [`Touch`] input in arbitrary order. pub fn iter_just_canceled(&self) -> impl Iterator { self.just_canceled.values() @@ -302,6 +338,25 @@ impl Touches { self.pressed.values().next().map(|t| t.position) } + /// Clears `just_pressed`, `just_released`, and `just_canceled` data for every touch input. + /// + /// See also [`Touches::reset_all`] for a full reset. + pub fn clear(&mut self) { + self.just_pressed.clear(); + self.just_released.clear(); + self.just_canceled.clear(); + } + + /// Clears `pressed`, `just_pressed`, `just_released`, and `just_canceled` data for every touch input. + /// + /// See also [`Touches::clear`] for clearing only touches that have just been pressed, released or canceled. + pub fn reset_all(&mut self) { + self.pressed.clear(); + self.just_pressed.clear(); + self.just_released.clear(); + self.just_canceled.clear(); + } + /// Processes a [`TouchInput`] event by updating the `pressed`, `just_pressed`, /// `just_released`, and `just_canceled` collections. fn process_touch_event(&mut self, event: &TouchInput) { @@ -408,6 +463,7 @@ mod test { #[test] fn touch_process() { use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; use bevy_math::Vec2; let mut touches = Touches::default(); @@ -417,6 +473,7 @@ mod test { let touch_event = TouchInput { phase: TouchPhase::Started, position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, force: None, id: 4, }; @@ -432,6 +489,7 @@ mod test { let moved_touch_event = TouchInput { phase: TouchPhase::Moved, position: Vec2::splat(5.0), + window: Entity::PLACEHOLDER, force: None, id: touch_event.id, }; @@ -453,6 +511,7 @@ mod test { let cancel_touch_event = TouchInput { phase: TouchPhase::Canceled, position: Vec2::ONE, + window: Entity::PLACEHOLDER, force: None, id: touch_event.id, }; @@ -468,6 +527,7 @@ mod test { let end_touch_event = TouchInput { phase: TouchPhase::Ended, position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, force: None, id: touch_event.id, }; @@ -487,6 +547,7 @@ mod test { #[test] fn touch_pressed() { use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; use bevy_math::Vec2; let mut touches = Touches::default(); @@ -494,6 +555,7 @@ mod test { let touch_event = TouchInput { phase: TouchPhase::Started, position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, force: None, id: 4, }; @@ -504,11 +566,15 @@ mod test { assert!(touches.get_pressed(touch_event.id).is_some()); assert!(touches.just_pressed(touch_event.id)); assert_eq!(touches.iter().count(), 1); + + touches.clear_just_pressed(touch_event.id); + assert!(!touches.just_pressed(touch_event.id)); } #[test] fn touch_released() { use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; use bevy_math::Vec2; let mut touches = Touches::default(); @@ -516,6 +582,7 @@ mod test { let touch_event = TouchInput { phase: TouchPhase::Ended, position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, force: None, id: 4, }; @@ -526,11 +593,15 @@ mod test { assert!(touches.get_released(touch_event.id).is_some()); assert!(touches.just_released(touch_event.id)); assert_eq!(touches.iter_just_released().count(), 1); + + touches.clear_just_released(touch_event.id); + assert!(!touches.just_released(touch_event.id)); } #[test] fn touch_canceled() { use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; use bevy_math::Vec2; let mut touches = Touches::default(); @@ -538,6 +609,7 @@ mod test { let touch_event = TouchInput { phase: TouchPhase::Canceled, position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, force: None, id: 4, }; @@ -547,5 +619,172 @@ mod test { assert!(touches.just_canceled(touch_event.id)); assert_eq!(touches.iter_just_canceled().count(), 1); + + touches.clear_just_canceled(touch_event.id); + assert!(!touches.just_canceled(touch_event.id)); + } + + #[test] + fn release_touch() { + use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; + use bevy_math::Vec2; + + let mut touches = Touches::default(); + + let touch_event = TouchInput { + phase: TouchPhase::Started, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 4, + }; + + // Register the touch and test that it was registered correctly + touches.process_touch_event(&touch_event); + + assert!(touches.get_pressed(touch_event.id).is_some()); + + touches.release(touch_event.id); + assert!(touches.get_pressed(touch_event.id).is_none()); + assert!(touches.just_released(touch_event.id)); + } + + #[test] + fn release_all_touches() { + use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; + use bevy_math::Vec2; + + let mut touches = Touches::default(); + + let touch_pressed_event = TouchInput { + phase: TouchPhase::Started, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 4, + }; + + let touch_moved_event = TouchInput { + phase: TouchPhase::Moved, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 4, + }; + + touches.process_touch_event(&touch_pressed_event); + touches.process_touch_event(&touch_moved_event); + + assert!(touches.get_pressed(touch_pressed_event.id).is_some()); + assert!(touches.get_pressed(touch_moved_event.id).is_some()); + + touches.release_all(); + + assert!(touches.get_pressed(touch_pressed_event.id).is_none()); + assert!(touches.just_released(touch_pressed_event.id)); + assert!(touches.get_pressed(touch_moved_event.id).is_none()); + assert!(touches.just_released(touch_moved_event.id)); + } + + #[test] + fn clear_touches() { + use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; + use bevy_math::Vec2; + + let mut touches = Touches::default(); + + let touch_press_event = TouchInput { + phase: TouchPhase::Started, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 4, + }; + + let touch_canceled_event = TouchInput { + phase: TouchPhase::Canceled, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 5, + }; + + let touch_released_event = TouchInput { + phase: TouchPhase::Ended, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 6, + }; + + // Register the touches and test that it was registered correctly + touches.process_touch_event(&touch_press_event); + touches.process_touch_event(&touch_canceled_event); + touches.process_touch_event(&touch_released_event); + + assert!(touches.get_pressed(touch_press_event.id).is_some()); + assert!(touches.just_pressed(touch_press_event.id)); + assert!(touches.just_canceled(touch_canceled_event.id)); + assert!(touches.just_released(touch_released_event.id)); + + touches.clear(); + + assert!(touches.get_pressed(touch_press_event.id).is_some()); + assert!(!touches.just_pressed(touch_press_event.id)); + assert!(!touches.just_canceled(touch_canceled_event.id)); + assert!(!touches.just_released(touch_released_event.id)); + } + + #[test] + fn reset_all_touches() { + use crate::{touch::TouchPhase, TouchInput, Touches}; + use bevy_ecs::entity::Entity; + use bevy_math::Vec2; + + let mut touches = Touches::default(); + + let touch_press_event = TouchInput { + phase: TouchPhase::Started, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 4, + }; + + let touch_canceled_event = TouchInput { + phase: TouchPhase::Canceled, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 5, + }; + + let touch_released_event = TouchInput { + phase: TouchPhase::Ended, + position: Vec2::splat(4.0), + window: Entity::PLACEHOLDER, + force: None, + id: 6, + }; + + // Register the touches and test that it was registered correctly + touches.process_touch_event(&touch_press_event); + touches.process_touch_event(&touch_canceled_event); + touches.process_touch_event(&touch_released_event); + + assert!(touches.get_pressed(touch_press_event.id).is_some()); + assert!(touches.just_pressed(touch_press_event.id)); + assert!(touches.just_canceled(touch_canceled_event.id)); + assert!(touches.just_released(touch_released_event.id)); + + touches.reset_all(); + + assert!(touches.get_pressed(touch_press_event.id).is_none()); + assert!(!touches.just_pressed(touch_press_event.id)); + assert!(!touches.just_canceled(touch_canceled_event.id)); + assert!(!touches.just_released(touch_released_event.id)); } } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 0563bb6a2122f..7e092e5a38dca 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -58,7 +58,10 @@ symphonia-vorbis = ["bevy_audio/symphonia-vorbis"] symphonia-wav = ["bevy_audio/symphonia-wav"] # Shader formats -shader_format_glsl = ["bevy_render/shader_format_glsl"] +shader_format_glsl = [ + "bevy_render/shader_format_glsl", + "bevy_pbr?/shader_format_glsl", +] shader_format_spirv = ["bevy_render/shader_format_spirv"] serialize = [ @@ -69,6 +72,7 @@ serialize = [ "bevy_transform/serialize", "bevy_math/serialize", "bevy_scene?/serialize", + "bevy_ui?/serialize", ] multi-threaded = [ "bevy_asset/multi-threaded", diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index d95957ab43dbf..0b87248f86a93 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -1,4 +1,4 @@ -use bevy_app::{PluginGroup, PluginGroupBuilder}; +use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder}; /// This plugin group will add all the default plugins for a *Bevy* application: /// * [`LogPlugin`](crate::log::LogPlugin) @@ -133,10 +133,52 @@ impl PluginGroup for DefaultPlugins { group = group.add(bevy_gizmos::GizmoPlugin); } + group = group.add(IgnoreAmbiguitiesPlugin); + group } } +struct IgnoreAmbiguitiesPlugin; + +impl Plugin for IgnoreAmbiguitiesPlugin { + #[allow(unused_variables)] // Variables are used depending on enabled features + fn build(&self, app: &mut bevy_app::App) { + // bevy_ui owns the Transform and cannot be animated + #[cfg(all(feature = "bevy_animation", feature = "bevy_ui"))] + app.ignore_ambiguity( + bevy_app::PostUpdate, + bevy_animation::animation_player, + bevy_ui::ui_layout_system, + ); + + #[cfg(feature = "bevy_render")] + if let Ok(render_app) = app.get_sub_app_mut(bevy_render::RenderApp) { + #[cfg(all(feature = "bevy_gizmos", feature = "bevy_sprite"))] + { + render_app.ignore_ambiguity( + bevy_render::Render, + bevy_gizmos::GizmoRenderSystem::QueueLineGizmos2d, + bevy_sprite::queue_sprites, + ); + render_app.ignore_ambiguity( + bevy_render::Render, + bevy_gizmos::GizmoRenderSystem::QueueLineGizmos2d, + bevy_sprite::queue_material2d_meshes::, + ); + } + #[cfg(all(feature = "bevy_gizmos", feature = "bevy_pbr"))] + { + render_app.ignore_ambiguity( + bevy_render::Render, + bevy_gizmos::GizmoRenderSystem::QueueLineGizmos3d, + bevy_pbr::queue_material_meshes::, + ); + } + } + } +} + /// This plugin group will add the minimal plugins for a *Bevy* application: /// * [`TaskPoolPlugin`](crate::core::TaskPoolPlugin) /// * [`TypeRegistrationPlugin`](crate::core::TypeRegistrationPlugin) diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 9e9344d0583c3..c8c70679fddce 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -22,10 +22,13 @@ tracing-subscriber = { version = "0.3.1", features = [ "env-filter", ] } tracing-chrome = { version = "0.7.0", optional = true } -tracing-tracy = { version = "0.10.0", optional = true } tracing-log = "0.1.2" tracing-error = { version = "0.2.0", optional = true } -tracy-client = { version = "0.16", optional = true } + +# Tracy dependency compatibility table: +# https://github.com/nagisa/rust_tracy_client +tracing-tracy = { version = "0.10.4", optional = true } +tracy-client = { version = "0.16.4", optional = true } [target.'cfg(target_os = "android")'.dependencies] android_log-sys = "0.3.0" diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index bab058535e4c2..b60c154dc64e2 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -11,8 +11,6 @@ //! For more fine-tuned control over logging behavior, set up the [`LogPlugin`] or //! `DefaultPlugins` during app initialization. -mod once; - #[cfg(feature = "trace")] use std::panic; @@ -31,15 +29,22 @@ pub mod prelude { debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, }; - pub use crate::{debug_once, error_once, info_once, trace_once, warn_once}; + #[doc(hidden)] + pub use bevy_utils::{debug_once, error_once, info_once, once, trace_once, warn_once}; } -pub use bevy_utils::tracing::{ - debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, - Level, +pub use bevy_utils::{ + debug_once, error_once, info_once, once, trace_once, + tracing::{ + debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, + Level, + }, + warn_once, }; +pub use tracing_subscriber; use bevy_app::{App, Plugin}; +use bevy_utils::tracing::Subscriber; use tracing_log::LogTracer; #[cfg(feature = "tracing-chrome")] use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; @@ -64,6 +69,7 @@ use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; /// .add_plugins(DefaultPlugins.set(LogPlugin { /// level: Level::DEBUG, /// filter: "wgpu=error,bevy_render=info,bevy_ecs=trace".to_string(), +/// update_subscriber: None, /// })) /// .run(); /// } @@ -100,13 +106,21 @@ pub struct LogPlugin { /// Filters out logs that are "less than" the given level. /// This can be further filtered using the `filter` setting. pub level: Level, + + /// Optionally apply extra transformations to the tracing subscriber. + /// For example add [`Layers`](tracing_subscriber::layer::Layer) + pub update_subscriber: Option BoxedSubscriber>, } +/// Alias for a boxed [`Subscriber`]. +pub type BoxedSubscriber = Box; + impl Default for LogPlugin { fn default() -> Self { Self { filter: "wgpu=error,naga=warn".to_string(), level: Level::INFO, + update_subscriber: None, } } } @@ -118,7 +132,7 @@ impl Plugin for LogPlugin { { let old_handler = panic::take_hook(); panic::set_hook(Box::new(move |infos| { - println!("{}", tracing_error::SpanTrace::capture()); + eprintln!("{}", tracing_error::SpanTrace::capture()); old_handler(infos); })); } @@ -179,7 +193,11 @@ impl Plugin for LogPlugin { #[cfg(feature = "tracing-tracy")] let subscriber = subscriber.with(tracy_layer); - finished_subscriber = subscriber; + if let Some(update_subscriber) = self.update_subscriber { + finished_subscriber = update_subscriber(Box::new(subscriber)); + } else { + finished_subscriber = Box::new(subscriber); + } } #[cfg(target_arch = "wasm32")] diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 88082b5a1ecec..90f5417168f9f 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] -toml_edit = "0.20" +toml_edit = { version = "0.21", default-features = false, features = ["parse"] } syn = "2.0" quote = "1.0" rustc-hash = "1.0" diff --git a/crates/bevy_macro_utils/src/fq_std.rs b/crates/bevy_macro_utils/src/fq_std.rs index ecaff7088f7e9..46bfdd43dab59 100644 --- a/crates/bevy_macro_utils/src/fq_std.rs +++ b/crates/bevy_macro_utils/src/fq_std.rs @@ -7,30 +7,33 @@ //! //! # Example //! Instead of writing this: -//! ```ignore +//! ``` +//! # use quote::quote; //! quote!( //! fn get_id() -> Option { //! Some(0) //! } -//! ) +//! ); //! ``` //! Or this: -//! ```ignore +//! ``` +//! # use quote::quote; //! quote!( //! fn get_id() -> ::core::option::Option { //! ::core::option::Option::Some(0) //! } -//! ) +//! ); //! ``` //! We should write this: -//! ```ignore -//! use crate::fq_std::FQOption; +//! ``` +//! use bevy_macro_utils::fq_std::FQOption; +//! # use quote::quote; //! //! quote!( //! fn get_id() -> #FQOption { //! #FQOption::Some(0) //! } -//! ) +//! ); //! ``` use proc_macro2::TokenStream; diff --git a/crates/bevy_macro_utils/src/label.rs b/crates/bevy_macro_utils/src/label.rs index 7facb10d1bb1b..a9fe177cb3cf8 100644 --- a/crates/bevy_macro_utils/src/label.rs +++ b/crates/bevy_macro_utils/src/label.rs @@ -79,7 +79,7 @@ pub fn derive_label( }) .unwrap(), ); - (quote! { + quote! { impl #impl_generics #trait_path for #ident #ty_generics #where_clause { fn dyn_clone(&self) -> ::std::boxed::Box { ::std::boxed::Box::new(::std::clone::Clone::clone(self)) @@ -95,6 +95,6 @@ pub fn derive_label( ::std::hash::Hash::hash(self, &mut state); } } - }) + } .into() } diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 7ee418a42158e..48b7959d481f0 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -9,13 +9,18 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] -glam = { version = "0.24.1", features = ["bytemuck"] } +glam = { version = "0.25", features = ["bytemuck"] } serde = { version = "1", features = ["derive"], optional = true } [features] serialize = ["dep:serde", "glam/serde"] +# Enable approx for glam types to approximate floating point equality comparisons and assertions +approx = ["glam/approx"] # Enable interoperation of glam types with mint-compatible libraries mint = ["glam/mint"] +# Enable libm mathematical functions for glam types to ensure consistent outputs +# across platforms at the cost of losing hardware-level optimization using intrinsics +libm = ["glam/libm"] # Enable assertions to check the validity of parameters passed to glam glam_assert = ["glam/glam-assert"] # Enable assertions in debug builds to check the validity of parameters passed to glam diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs new file mode 100644 index 0000000000000..252c507192c62 --- /dev/null +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -0,0 +1,446 @@ +mod primitive_impls; + +use glam::Mat2; + +use super::BoundingVolume; +use crate::prelude::Vec2; + +/// Computes the geometric center of the given set of points. +#[inline(always)] +fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 { + assert!( + !points.is_empty(), + "cannot compute the center of an empty set of points" + ); + + let denom = 1.0 / points.len() as f32; + points.iter().fold(Vec2::ZERO, |acc, point| acc + *point) * denom +} + +/// A trait with methods that return 2D bounded volumes for a shape +pub trait Bounded2d { + /// Get an axis-aligned bounding box for the shape with the given translation and rotation. + /// The rotation is in radians, counterclockwise, with 0 meaning no rotation. + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d; + /// Get a bounding circle for the shape + /// The rotation is in radians, counterclockwise, with 0 meaning no rotation. + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle; +} + +/// A 2D axis-aligned bounding box, or bounding rectangle +#[doc(alias = "BoundingRectangle")] +#[derive(Clone, Debug)] +pub struct Aabb2d { + /// The minimum, conventionally bottom-left, point of the box + pub min: Vec2, + /// The maximum, conventionally top-right, point of the box + pub max: Vec2, +} + +impl Aabb2d { + /// Constructs an AABB from its center and half-size. + #[inline(always)] + pub fn new(center: Vec2, half_size: Vec2) -> Self { + debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0); + Self { + min: center - half_size, + max: center + half_size, + } + } + + /// Computes the smallest [`Aabb2d`] containing the given set of points, + /// transformed by `translation` and `rotation`. + /// + /// # Panics + /// + /// Panics if the given set of points is empty. + #[inline(always)] + pub fn from_point_cloud(translation: Vec2, rotation: f32, points: &[Vec2]) -> Aabb2d { + // Transform all points by rotation + let rotation_mat = Mat2::from_angle(rotation); + let mut iter = points.iter().map(|point| rotation_mat * *point); + + let first = iter + .next() + .expect("point cloud must contain at least one point for Aabb2d construction"); + + let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| { + (point.min(prev_min), point.max(prev_max)) + }); + + Aabb2d { + min: min + translation, + max: max + translation, + } + } + + /// Computes the smallest [`BoundingCircle`] containing this [`Aabb2d`]. + #[inline(always)] + pub fn bounding_circle(&self) -> BoundingCircle { + let radius = self.min.distance(self.max) / 2.0; + BoundingCircle::new(self.center(), radius) + } +} + +impl BoundingVolume for Aabb2d { + type Position = Vec2; + type HalfSize = Vec2; + + #[inline(always)] + fn center(&self) -> Self::Position { + (self.min + self.max) / 2. + } + + #[inline(always)] + fn half_size(&self) -> Self::HalfSize { + (self.max - self.min) / 2. + } + + #[inline(always)] + fn visible_area(&self) -> f32 { + let b = self.max - self.min; + b.x * b.y + } + + #[inline(always)] + fn contains(&self, other: &Self) -> bool { + other.min.x >= self.min.x + && other.min.y >= self.min.y + && other.max.x <= self.max.x + && other.max.y <= self.max.y + } + + #[inline(always)] + fn merge(&self, other: &Self) -> Self { + Self { + min: self.min.min(other.min), + max: self.max.max(other.max), + } + } + + #[inline(always)] + fn grow(&self, amount: Self::HalfSize) -> Self { + let b = Self { + min: self.min - amount, + max: self.max + amount, + }; + debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y); + b + } + + #[inline(always)] + fn shrink(&self, amount: Self::HalfSize) -> Self { + let b = Self { + min: self.min + amount, + max: self.max - amount, + }; + debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y); + b + } +} + +#[cfg(test)] +mod aabb2d_tests { + use super::Aabb2d; + use crate::{bounding::BoundingVolume, Vec2}; + + #[test] + fn center() { + let aabb = Aabb2d { + min: Vec2::new(-0.5, -1.), + max: Vec2::new(1., 1.), + }; + assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < std::f32::EPSILON); + let aabb = Aabb2d { + min: Vec2::new(5., -10.), + max: Vec2::new(10., -5.), + }; + assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < std::f32::EPSILON); + } + + #[test] + fn half_size() { + let aabb = Aabb2d { + min: Vec2::new(-0.5, -1.), + max: Vec2::new(1., 1.), + }; + let half_size = aabb.half_size(); + assert!((half_size - Vec2::new(0.75, 1.)).length() < std::f32::EPSILON); + } + + #[test] + fn area() { + let aabb = Aabb2d { + min: Vec2::new(-1., -1.), + max: Vec2::new(1., 1.), + }; + assert!((aabb.visible_area() - 4.).abs() < std::f32::EPSILON); + let aabb = Aabb2d { + min: Vec2::new(0., 0.), + max: Vec2::new(1., 0.5), + }; + assert!((aabb.visible_area() - 0.5).abs() < std::f32::EPSILON); + } + + #[test] + fn contains() { + let a = Aabb2d { + min: Vec2::new(-1., -1.), + max: Vec2::new(1., 1.), + }; + let b = Aabb2d { + min: Vec2::new(-2., -1.), + max: Vec2::new(1., 1.), + }; + assert!(!a.contains(&b)); + let b = Aabb2d { + min: Vec2::new(-0.25, -0.8), + max: Vec2::new(1., 1.), + }; + assert!(a.contains(&b)); + } + + #[test] + fn merge() { + let a = Aabb2d { + min: Vec2::new(-1., -1.), + max: Vec2::new(1., 0.5), + }; + let b = Aabb2d { + min: Vec2::new(-2., -0.5), + max: Vec2::new(0.75, 1.), + }; + let merged = a.merge(&b); + assert!((merged.min - Vec2::new(-2., -1.)).length() < std::f32::EPSILON); + assert!((merged.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON); + assert!(merged.contains(&a)); + assert!(merged.contains(&b)); + assert!(!a.contains(&merged)); + assert!(!b.contains(&merged)); + } + + #[test] + fn grow() { + let a = Aabb2d { + min: Vec2::new(-1., -1.), + max: Vec2::new(1., 1.), + }; + let padded = a.grow(Vec2::ONE); + assert!((padded.min - Vec2::new(-2., -2.)).length() < std::f32::EPSILON); + assert!((padded.max - Vec2::new(2., 2.)).length() < std::f32::EPSILON); + assert!(padded.contains(&a)); + assert!(!a.contains(&padded)); + } + + #[test] + fn shrink() { + let a = Aabb2d { + min: Vec2::new(-2., -2.), + max: Vec2::new(2., 2.), + }; + let shrunk = a.shrink(Vec2::ONE); + assert!((shrunk.min - Vec2::new(-1., -1.)).length() < std::f32::EPSILON); + assert!((shrunk.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON); + assert!(a.contains(&shrunk)); + assert!(!shrunk.contains(&a)); + } +} + +use crate::primitives::Circle; + +/// A bounding circle +#[derive(Clone, Debug)] +pub struct BoundingCircle { + /// The center of the bounding circle + pub center: Vec2, + /// The circle + pub circle: Circle, +} + +impl BoundingCircle { + /// Constructs a bounding circle from its center and radius. + #[inline(always)] + pub fn new(center: Vec2, radius: f32) -> Self { + debug_assert!(radius >= 0.); + Self { + center, + circle: Circle { radius }, + } + } + + /// Computes a [`BoundingCircle`] containing the given set of points, + /// transformed by `translation` and `rotation`. + /// + /// The bounding circle is not guaranteed to be the smallest possible. + #[inline(always)] + pub fn from_point_cloud(translation: Vec2, rotation: f32, points: &[Vec2]) -> BoundingCircle { + let center = point_cloud_2d_center(points); + let mut radius_squared = 0.0; + + for point in points { + // Get squared version to avoid unnecessary sqrt calls + let distance_squared = point.distance_squared(center); + if distance_squared > radius_squared { + radius_squared = distance_squared; + } + } + + BoundingCircle::new( + Mat2::from_angle(rotation) * center + translation, + radius_squared.sqrt(), + ) + } + + /// Get the radius of the bounding circle + #[inline(always)] + pub fn radius(&self) -> f32 { + self.circle.radius + } + + /// Computes the smallest [`Aabb2d`] containing this [`BoundingCircle`]. + #[inline(always)] + pub fn aabb_2d(&self) -> Aabb2d { + Aabb2d { + min: self.center - Vec2::splat(self.radius()), + max: self.center + Vec2::splat(self.radius()), + } + } +} + +impl BoundingVolume for BoundingCircle { + type Position = Vec2; + type HalfSize = f32; + + #[inline(always)] + fn center(&self) -> Self::Position { + self.center + } + + #[inline(always)] + fn half_size(&self) -> Self::HalfSize { + self.radius() + } + + #[inline(always)] + fn visible_area(&self) -> f32 { + std::f32::consts::PI * self.radius() * self.radius() + } + + #[inline(always)] + fn contains(&self, other: &Self) -> bool { + let diff = self.radius() - other.radius(); + self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff) + } + + #[inline(always)] + fn merge(&self, other: &Self) -> Self { + let diff = other.center - self.center; + let length = diff.length(); + if self.radius() >= length + other.radius() { + return self.clone(); + } + if other.radius() >= length + self.radius() { + return other.clone(); + } + let dir = diff / length; + Self::new( + (self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.), + (length + self.radius() + other.radius()) / 2., + ) + } + + #[inline(always)] + fn grow(&self, amount: Self::HalfSize) -> Self { + debug_assert!(amount >= 0.); + Self::new(self.center, self.radius() + amount) + } + + #[inline(always)] + fn shrink(&self, amount: Self::HalfSize) -> Self { + debug_assert!(amount >= 0.); + debug_assert!(self.radius() >= amount); + Self::new(self.center, self.radius() - amount) + } +} + +#[cfg(test)] +mod bounding_circle_tests { + use super::BoundingCircle; + use crate::{bounding::BoundingVolume, Vec2}; + + #[test] + fn area() { + let circle = BoundingCircle::new(Vec2::ONE, 5.); + // Since this number is messy we check it with a higher threshold + assert!((circle.visible_area() - 78.5398).abs() < 0.001); + } + + #[test] + fn contains() { + let a = BoundingCircle::new(Vec2::ONE, 5.); + let b = BoundingCircle::new(Vec2::new(5.5, 1.), 1.); + assert!(!a.contains(&b)); + let b = BoundingCircle::new(Vec2::new(1., -3.5), 0.5); + assert!(a.contains(&b)); + } + + #[test] + fn contains_identical() { + let a = BoundingCircle::new(Vec2::ONE, 5.); + assert!(a.contains(&a)); + } + + #[test] + fn merge() { + // When merging two circles that don't contain each other, we find a center position that + // contains both + let a = BoundingCircle::new(Vec2::ONE, 5.); + let b = BoundingCircle::new(Vec2::new(1., -4.), 1.); + let merged = a.merge(&b); + assert!((merged.center - Vec2::new(1., 0.5)).length() < std::f32::EPSILON); + assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON); + assert!(merged.contains(&a)); + assert!(merged.contains(&b)); + assert!(!a.contains(&merged)); + assert!(!b.contains(&merged)); + + // When one circle contains the other circle, we use the bigger circle + let b = BoundingCircle::new(Vec2::ZERO, 3.); + assert!(a.contains(&b)); + let merged = a.merge(&b); + assert_eq!(merged.center, a.center); + assert_eq!(merged.radius(), a.radius()); + + // When two circles are at the same point, we use the bigger radius + let b = BoundingCircle::new(Vec2::ONE, 6.); + let merged = a.merge(&b); + assert_eq!(merged.center, a.center); + assert_eq!(merged.radius(), b.radius()); + } + + #[test] + fn merge_identical() { + let a = BoundingCircle::new(Vec2::ONE, 5.); + let merged = a.merge(&a); + assert_eq!(merged.center, a.center); + assert_eq!(merged.radius(), a.radius()); + } + + #[test] + fn grow() { + let a = BoundingCircle::new(Vec2::ONE, 5.); + let padded = a.grow(1.25); + assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON); + assert!(padded.contains(&a)); + assert!(!a.contains(&padded)); + } + + #[test] + fn shrink() { + let a = BoundingCircle::new(Vec2::ONE, 5.); + let shrunk = a.shrink(0.5); + assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON); + assert!(a.contains(&shrunk)); + assert!(!shrunk.contains(&a)); + } +} diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs new file mode 100644 index 0000000000000..056c3d388eb3e --- /dev/null +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -0,0 +1,443 @@ +//! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). + +use glam::{Mat2, Vec2}; + +use crate::primitives::{ + BoxedPolygon, BoxedPolyline2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, + Rectangle, RegularPolygon, Segment2d, Triangle2d, +}; + +use super::{Aabb2d, Bounded2d, BoundingCircle}; + +impl Bounded2d for Circle { + fn aabb_2d(&self, translation: Vec2, _rotation: f32) -> Aabb2d { + Aabb2d::new(translation, Vec2::splat(self.radius)) + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, self.radius) + } +} + +impl Bounded2d for Ellipse { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + // V = (hh * cos(beta), hh * sin(beta)) + // #####*##### + // ### | ### + // # hh | # + // # *---------* U = (hw * cos(alpha), hw * sin(alpha)) + // # hw # + // ### ### + // ########### + + let (hw, hh) = (self.half_size.x, self.half_size.y); + + // Sine and cosine of rotation angle alpha. + let (alpha_sin, alpha_cos) = rotation.sin_cos(); + + // Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions: + // sin(beta) = sin(alpha + pi/2) = cos(alpha) + // cos(beta) = cos(alpha + pi/2) = -sin(alpha) + let (beta_sin, beta_cos) = (alpha_cos, -alpha_sin); + + // Compute points U and V, the extremes of the ellipse + let (ux, uy) = (hw * alpha_cos, hw * alpha_sin); + let (vx, vy) = (hh * beta_cos, hh * beta_sin); + + let half_size = Vec2::new(ux.hypot(vx), uy.hypot(vy)); + + Aabb2d::new(translation, half_size) + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, self.semi_major()) + } +} + +impl Bounded2d for Plane2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + let normal = Mat2::from_angle(rotation) * *self.normal; + let facing_x = normal == Vec2::X || normal == Vec2::NEG_X; + let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; + let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; + let half_size = Vec2::new(half_width, half_height); + + Aabb2d::new(translation, half_size) + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, f32::MAX / 2.0) + } +} + +impl Bounded2d for Line2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + let direction = Mat2::from_angle(rotation) * *self.direction; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let max = f32::MAX / 2.0; + let half_width = if direction.x == 0.0 { 0.0 } else { max }; + let half_height = if direction.y == 0.0 { 0.0 } else { max }; + let half_size = Vec2::new(half_width, half_height); + + Aabb2d::new(translation, half_size) + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, f32::MAX / 2.0) + } +} + +impl Bounded2d for Segment2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + // Rotate the segment by `rotation` + let direction = Mat2::from_angle(rotation) * *self.direction; + let half_size = (self.half_length * direction).abs(); + + Aabb2d::new(translation, half_size) + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, self.half_length) + } +} + +impl Bounded2d for Polyline2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + } +} + +impl Bounded2d for BoxedPolyline2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + } +} + +impl Bounded2d for Triangle2d { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + let rotation_mat = Mat2::from_angle(rotation); + let [a, b, c] = self.vertices.map(|vtx| rotation_mat * vtx); + + let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y)); + let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y)); + + Aabb2d { + min: min + translation, + max: max + translation, + } + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + let rotation_mat = Mat2::from_angle(rotation); + let [a, b, c] = self.vertices; + + // The points of the segment opposite to the obtuse or right angle if one exists + let side_opposite_to_non_acute = if (b - a).dot(c - a) <= 0.0 { + Some((b, c)) + } else if (c - b).dot(a - b) <= 0.0 { + Some((c, a)) + } else if (a - c).dot(b - c) <= 0.0 { + Some((a, b)) + } else { + // The triangle is acute. + None + }; + + // Find the minimum bounding circle. If the triangle is obtuse, the circle passes through two vertices. + // Otherwise, it's the circumcircle and passes through all three. + if let Some((point1, point2)) = side_opposite_to_non_acute { + // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side. + // We can compute the minimum bounding circle from the line segment of the longest side. + let (segment, center) = Segment2d::from_points(point1, point2); + segment.bounding_circle(rotation_mat * center + translation, rotation) + } else { + // The triangle is acute, so the smallest bounding circle is the circumcircle. + let (Circle { radius }, circumcenter) = self.circumcircle(); + BoundingCircle::new(rotation_mat * circumcenter + translation, radius) + } + } +} + +impl Bounded2d for Rectangle { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + // Compute the AABB of the rotated rectangle by transforming the half-extents + // by an absolute rotation matrix. + let (sin, cos) = rotation.sin_cos(); + let abs_rot_mat = Mat2::from_cols_array(&[cos.abs(), sin.abs(), sin.abs(), cos.abs()]); + let half_size = abs_rot_mat * self.half_size; + + Aabb2d::new(translation, half_size) + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + let radius = self.half_size.length(); + BoundingCircle::new(translation, radius) + } +} + +impl Bounded2d for Polygon { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + } +} + +impl Bounded2d for BoxedPolygon { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + } + + fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + } +} + +impl Bounded2d for RegularPolygon { + fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + let mut min = Vec2::ZERO; + let mut max = Vec2::ZERO; + + for vertex in self.vertices(rotation) { + min = min.min(vertex); + max = max.max(vertex); + } + + Aabb2d { + min: min + translation, + max: max + translation, + } + } + + fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + BoundingCircle::new(translation, self.circumcircle.radius) + } +} + +#[cfg(test)] +mod tests { + use glam::Vec2; + + use crate::{ + bounding::Bounded2d, + primitives::{ + Circle, Direction2d, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, + RegularPolygon, Segment2d, Triangle2d, + }, + }; + + #[test] + fn circle() { + let circle = Circle { radius: 1.0 }; + let translation = Vec2::new(2.0, 1.0); + + let aabb = circle.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + let bounding_circle = circle.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + } + + #[test] + fn ellipse() { + let ellipse = Ellipse::new(1.0, 0.5); + let translation = Vec2::new(2.0, 1.0); + + let aabb = ellipse.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); + assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); + + let bounding_circle = ellipse.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + } + + #[test] + fn plane() { + let translation = Vec2::new(2.0, 1.0); + + let aabb1 = Plane2d::new(Vec2::X).aabb_2d(translation, 0.0); + assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); + assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); + + let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(translation, 0.0); + assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); + assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); + + let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(translation, 0.0); + assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); + assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); + + let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); + } + + #[test] + fn line() { + let translation = Vec2::new(2.0, 1.0); + + let aabb1 = Line2d { + direction: Direction2d::Y, + } + .aabb_2d(translation, 0.0); + assert_eq!(aabb1.min, Vec2::new(2.0, -f32::MAX / 2.0)); + assert_eq!(aabb1.max, Vec2::new(2.0, f32::MAX / 2.0)); + + let aabb2 = Line2d { + direction: Direction2d::X, + } + .aabb_2d(translation, 0.0); + assert_eq!(aabb2.min, Vec2::new(-f32::MAX / 2.0, 1.0)); + assert_eq!(aabb2.max, Vec2::new(f32::MAX / 2.0, 1.0)); + + let aabb3 = Line2d { + direction: Direction2d::from_xy(1.0, 1.0).unwrap(), + } + .aabb_2d(translation, 0.0); + assert_eq!(aabb3.min, Vec2::new(-f32::MAX / 2.0, -f32::MAX / 2.0)); + assert_eq!(aabb3.max, Vec2::new(f32::MAX / 2.0, f32::MAX / 2.0)); + + let bounding_circle = Line2d { + direction: Direction2d::Y, + } + .bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); + } + + #[test] + fn segment() { + let translation = Vec2::new(2.0, 1.0); + let segment = Segment2d::from_points(Vec2::new(-1.0, -0.5), Vec2::new(1.0, 0.5)).0; + + let aabb = segment.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(1.0, 0.5)); + assert_eq!(aabb.max, Vec2::new(3.0, 1.5)); + + let bounding_circle = segment.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5)); + } + + #[test] + fn polyline() { + let polyline = Polyline2d::<4>::new([ + Vec2::ONE, + Vec2::new(-1.0, 1.0), + Vec2::NEG_ONE, + Vec2::new(1.0, -1.0), + ]); + let translation = Vec2::new(2.0, 1.0); + + let aabb = polyline.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + let bounding_circle = polyline.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2); + } + + #[test] + fn acute_triangle() { + let acute_triangle = + Triangle2d::new(Vec2::new(0.0, 1.0), Vec2::NEG_ONE, Vec2::new(1.0, -1.0)); + let translation = Vec2::new(2.0, 1.0); + + let aabb = acute_triangle.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + // For acute triangles, the center is the circumcenter + let (Circle { radius }, circumcenter) = acute_triangle.circumcircle(); + let bounding_circle = acute_triangle.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, circumcenter + translation); + assert_eq!(bounding_circle.radius(), radius); + } + + #[test] + fn obtuse_triangle() { + let obtuse_triangle = Triangle2d::new( + Vec2::new(0.0, 1.0), + Vec2::new(-10.0, -1.0), + Vec2::new(10.0, -1.0), + ); + let translation = Vec2::new(2.0, 1.0); + + let aabb = obtuse_triangle.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(-8.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(12.0, 2.0)); + + // For obtuse and right triangles, the center is the midpoint of the longest side (diameter of bounding circle) + let bounding_circle = obtuse_triangle.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation - Vec2::Y); + assert_eq!(bounding_circle.radius(), 10.0); + } + + #[test] + fn rectangle() { + let rectangle = Rectangle::new(2.0, 1.0); + let translation = Vec2::new(2.0, 1.0); + + let aabb = rectangle.aabb_2d(translation, std::f32::consts::FRAC_PI_4); + let expected_half_size = Vec2::splat(1.0606601); + assert_eq!(aabb.min, translation - expected_half_size); + assert_eq!(aabb.max, translation + expected_half_size); + + let bounding_circle = rectangle.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5)); + } + + #[test] + fn polygon() { + let polygon = Polygon::<4>::new([ + Vec2::ONE, + Vec2::new(-1.0, 1.0), + Vec2::NEG_ONE, + Vec2::new(1.0, -1.0), + ]); + let translation = Vec2::new(2.0, 1.0); + + let aabb = polygon.aabb_2d(translation, 0.0); + assert_eq!(aabb.min, Vec2::new(1.0, 0.0)); + assert_eq!(aabb.max, Vec2::new(3.0, 2.0)); + + let bounding_circle = polygon.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2); + } + + #[test] + fn regular_polygon() { + let regular_polygon = RegularPolygon::new(1.0, 5); + let translation = Vec2::new(2.0, 1.0); + + let aabb = regular_polygon.aabb_2d(translation, 0.0); + assert!((aabb.min - (translation - Vec2::new(0.9510565, 0.8090169))).length() < 1e-6); + assert!((aabb.max - (translation + Vec2::new(0.9510565, 1.0))).length() < 1e-6); + + let bounding_circle = regular_polygon.bounding_circle(translation, 0.0); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 1.0); + } +} diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs new file mode 100644 index 0000000000000..65c843a79f3dd --- /dev/null +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -0,0 +1,447 @@ +mod primitive_impls; + +use super::BoundingVolume; +use crate::prelude::{Quat, Vec3}; + +/// Computes the geometric center of the given set of points. +#[inline(always)] +fn point_cloud_3d_center(points: &[Vec3]) -> Vec3 { + assert!( + !points.is_empty(), + "cannot compute the center of an empty set of points" + ); + + let denom = 1.0 / points.len() as f32; + points.iter().fold(Vec3::ZERO, |acc, point| acc + *point) * denom +} + +/// A trait with methods that return 3D bounded volumes for a shape +pub trait Bounded3d { + /// Get an axis-aligned bounding box for the shape with the given translation and rotation + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d; + /// Get a bounding sphere for the shape with the given translation and rotation + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere; +} + +/// A 3D axis-aligned bounding box +#[derive(Clone, Debug)] +pub struct Aabb3d { + /// The minimum point of the box + pub min: Vec3, + /// The maximum point of the box + pub max: Vec3, +} + +impl Aabb3d { + /// Constructs an AABB from its center and half-size. + #[inline(always)] + pub fn new(center: Vec3, half_size: Vec3) -> Self { + debug_assert!(half_size.x >= 0.0 && half_size.y >= 0.0 && half_size.z >= 0.0); + Self { + min: center - half_size, + max: center + half_size, + } + } + + /// Computes the smallest [`Aabb3d`] containing the given set of points, + /// transformed by `translation` and `rotation`. + /// + /// # Panics + /// + /// Panics if the given set of points is empty. + #[inline(always)] + pub fn from_point_cloud(translation: Vec3, rotation: Quat, points: &[Vec3]) -> Aabb3d { + // Transform all points by rotation + let mut iter = points.iter().map(|point| rotation * *point); + + let first = iter + .next() + .expect("point cloud must contain at least one point for Aabb3d construction"); + + let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| { + (point.min(prev_min), point.max(prev_max)) + }); + + Aabb3d { + min: min + translation, + max: max + translation, + } + } + + /// Computes the smallest [`BoundingSphere`] containing this [`Aabb3d`]. + #[inline(always)] + pub fn bounding_sphere(&self) -> BoundingSphere { + let radius = self.min.distance(self.max) / 2.0; + BoundingSphere::new(self.center(), radius) + } +} + +impl BoundingVolume for Aabb3d { + type Position = Vec3; + type HalfSize = Vec3; + + #[inline(always)] + fn center(&self) -> Self::Position { + (self.min + self.max) / 2. + } + + #[inline(always)] + fn half_size(&self) -> Self::HalfSize { + (self.max - self.min) / 2. + } + + #[inline(always)] + fn visible_area(&self) -> f32 { + let b = self.max - self.min; + b.x * (b.y + b.z) + b.y * b.z + } + + #[inline(always)] + fn contains(&self, other: &Self) -> bool { + other.min.x >= self.min.x + && other.min.y >= self.min.y + && other.min.z >= self.min.z + && other.max.x <= self.max.x + && other.max.y <= self.max.y + && other.max.z <= self.max.z + } + + #[inline(always)] + fn merge(&self, other: &Self) -> Self { + Self { + min: self.min.min(other.min), + max: self.max.max(other.max), + } + } + + #[inline(always)] + fn grow(&self, amount: Self::HalfSize) -> Self { + let b = Self { + min: self.min - amount, + max: self.max + amount, + }; + debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y && b.min.z <= b.max.z); + b + } + + #[inline(always)] + fn shrink(&self, amount: Self::HalfSize) -> Self { + let b = Self { + min: self.min + amount, + max: self.max - amount, + }; + debug_assert!(b.min.x <= b.max.x && b.min.y <= b.max.y && b.min.z <= b.max.z); + b + } +} + +#[cfg(test)] +mod aabb3d_tests { + use super::Aabb3d; + use crate::{bounding::BoundingVolume, Vec3}; + + #[test] + fn center() { + let aabb = Aabb3d { + min: Vec3::new(-0.5, -1., -0.5), + max: Vec3::new(1., 1., 2.), + }; + assert!((aabb.center() - Vec3::new(0.25, 0., 0.75)).length() < std::f32::EPSILON); + let aabb = Aabb3d { + min: Vec3::new(5., 5., -10.), + max: Vec3::new(10., 10., -5.), + }; + assert!((aabb.center() - Vec3::new(7.5, 7.5, -7.5)).length() < std::f32::EPSILON); + } + + #[test] + fn half_size() { + let aabb = Aabb3d { + min: Vec3::new(-0.5, -1., -0.5), + max: Vec3::new(1., 1., 2.), + }; + assert!((aabb.half_size() - Vec3::new(0.75, 1., 1.25)).length() < std::f32::EPSILON); + } + + #[test] + fn area() { + let aabb = Aabb3d { + min: Vec3::new(-1., -1., -1.), + max: Vec3::new(1., 1., 1.), + }; + assert!((aabb.visible_area() - 12.).abs() < std::f32::EPSILON); + let aabb = Aabb3d { + min: Vec3::new(0., 0., 0.), + max: Vec3::new(1., 0.5, 0.25), + }; + assert!((aabb.visible_area() - 0.875).abs() < std::f32::EPSILON); + } + + #[test] + fn contains() { + let a = Aabb3d { + min: Vec3::new(-1., -1., -1.), + max: Vec3::new(1., 1., 1.), + }; + let b = Aabb3d { + min: Vec3::new(-2., -1., -1.), + max: Vec3::new(1., 1., 1.), + }; + assert!(!a.contains(&b)); + let b = Aabb3d { + min: Vec3::new(-0.25, -0.8, -0.9), + max: Vec3::new(1., 1., 0.9), + }; + assert!(a.contains(&b)); + } + + #[test] + fn merge() { + let a = Aabb3d { + min: Vec3::new(-1., -1., -1.), + max: Vec3::new(1., 0.5, 1.), + }; + let b = Aabb3d { + min: Vec3::new(-2., -0.5, -0.), + max: Vec3::new(0.75, 1., 2.), + }; + let merged = a.merge(&b); + assert!((merged.min - Vec3::new(-2., -1., -1.)).length() < std::f32::EPSILON); + assert!((merged.max - Vec3::new(1., 1., 2.)).length() < std::f32::EPSILON); + assert!(merged.contains(&a)); + assert!(merged.contains(&b)); + assert!(!a.contains(&merged)); + assert!(!b.contains(&merged)); + } + + #[test] + fn grow() { + let a = Aabb3d { + min: Vec3::new(-1., -1., -1.), + max: Vec3::new(1., 1., 1.), + }; + let padded = a.grow(Vec3::ONE); + assert!((padded.min - Vec3::new(-2., -2., -2.)).length() < std::f32::EPSILON); + assert!((padded.max - Vec3::new(2., 2., 2.)).length() < std::f32::EPSILON); + assert!(padded.contains(&a)); + assert!(!a.contains(&padded)); + } + + #[test] + fn shrink() { + let a = Aabb3d { + min: Vec3::new(-2., -2., -2.), + max: Vec3::new(2., 2., 2.), + }; + let shrunk = a.shrink(Vec3::ONE); + assert!((shrunk.min - Vec3::new(-1., -1., -1.)).length() < std::f32::EPSILON); + assert!((shrunk.max - Vec3::new(1., 1., 1.)).length() < std::f32::EPSILON); + assert!(a.contains(&shrunk)); + assert!(!shrunk.contains(&a)); + } +} + +use crate::primitives::Sphere; + +/// A bounding sphere +#[derive(Clone, Debug)] +pub struct BoundingSphere { + /// The center of the bounding sphere + pub center: Vec3, + /// The sphere + pub sphere: Sphere, +} + +impl BoundingSphere { + /// Constructs a bounding sphere from its center and radius. + pub fn new(center: Vec3, radius: f32) -> Self { + debug_assert!(radius >= 0.); + Self { + center, + sphere: Sphere { radius }, + } + } + + /// Computes a [`BoundingSphere`] containing the given set of points, + /// transformed by `translation` and `rotation`. + /// + /// The bounding sphere is not guaranteed to be the smallest possible. + #[inline(always)] + pub fn from_point_cloud(translation: Vec3, rotation: Quat, points: &[Vec3]) -> BoundingSphere { + let center = point_cloud_3d_center(points); + let mut radius_squared = 0.0; + + for point in points { + // Get squared version to avoid unnecessary sqrt calls + let distance_squared = point.distance_squared(center); + if distance_squared > radius_squared { + radius_squared = distance_squared; + } + } + + BoundingSphere::new(rotation * center + translation, radius_squared.sqrt()) + } + + /// Get the radius of the bounding sphere + #[inline(always)] + pub fn radius(&self) -> f32 { + self.sphere.radius + } + + /// Computes the smallest [`Aabb3d`] containing this [`BoundingSphere`]. + #[inline(always)] + pub fn aabb_3d(&self) -> Aabb3d { + Aabb3d { + min: self.center - Vec3::splat(self.radius()), + max: self.center + Vec3::splat(self.radius()), + } + } +} + +impl BoundingVolume for BoundingSphere { + type Position = Vec3; + type HalfSize = f32; + + #[inline(always)] + fn center(&self) -> Self::Position { + self.center + } + + #[inline(always)] + fn half_size(&self) -> Self::HalfSize { + self.radius() + } + + #[inline(always)] + fn visible_area(&self) -> f32 { + 2. * std::f32::consts::PI * self.radius() * self.radius() + } + + #[inline(always)] + fn contains(&self, other: &Self) -> bool { + let diff = self.radius() - other.radius(); + self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff) + } + + #[inline(always)] + fn merge(&self, other: &Self) -> Self { + let diff = other.center - self.center; + let length = diff.length(); + if self.radius() >= length + other.radius() { + return self.clone(); + } + if other.radius() >= length + self.radius() { + return other.clone(); + } + let dir = diff / length; + Self::new( + (self.center + other.center) / 2. + dir * ((other.radius() - self.radius()) / 2.), + (length + self.radius() + other.radius()) / 2., + ) + } + + #[inline(always)] + fn grow(&self, amount: Self::HalfSize) -> Self { + debug_assert!(amount >= 0.); + Self { + center: self.center, + sphere: Sphere { + radius: self.radius() + amount, + }, + } + } + + #[inline(always)] + fn shrink(&self, amount: Self::HalfSize) -> Self { + debug_assert!(amount >= 0.); + debug_assert!(self.radius() >= amount); + Self { + center: self.center, + sphere: Sphere { + radius: self.radius() - amount, + }, + } + } +} + +#[cfg(test)] +mod bounding_sphere_tests { + use super::BoundingSphere; + use crate::{bounding::BoundingVolume, Vec3}; + + #[test] + fn area() { + let sphere = BoundingSphere::new(Vec3::ONE, 5.); + // Since this number is messy we check it with a higher threshold + assert!((sphere.visible_area() - 157.0796).abs() < 0.001); + } + + #[test] + fn contains() { + let a = BoundingSphere::new(Vec3::ONE, 5.); + let b = BoundingSphere::new(Vec3::new(5.5, 1., 1.), 1.); + assert!(!a.contains(&b)); + let b = BoundingSphere::new(Vec3::new(1., -3.5, 1.), 0.5); + assert!(a.contains(&b)); + } + + #[test] + fn contains_identical() { + let a = BoundingSphere::new(Vec3::ONE, 5.); + assert!(a.contains(&a)); + } + + #[test] + fn merge() { + // When merging two circles that don't contain each other, we find a center position that + // contains both + let a = BoundingSphere::new(Vec3::ONE, 5.); + let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.); + let merged = a.merge(&b); + assert!((merged.center - Vec3::new(1., 1., 0.5)).length() < std::f32::EPSILON); + assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON); + assert!(merged.contains(&a)); + assert!(merged.contains(&b)); + assert!(!a.contains(&merged)); + assert!(!b.contains(&merged)); + + // When one circle contains the other circle, we use the bigger circle + let b = BoundingSphere::new(Vec3::ZERO, 3.); + assert!(a.contains(&b)); + let merged = a.merge(&b); + assert_eq!(merged.center, a.center); + assert_eq!(merged.radius(), a.radius()); + + // When two circles are at the same point, we use the bigger radius + let b = BoundingSphere::new(Vec3::ONE, 6.); + let merged = a.merge(&b); + assert_eq!(merged.center, a.center); + assert_eq!(merged.radius(), b.radius()); + } + + #[test] + fn merge_identical() { + let a = BoundingSphere::new(Vec3::ONE, 5.); + let merged = a.merge(&a); + assert_eq!(merged.center, a.center); + assert_eq!(merged.radius(), a.radius()); + } + + #[test] + fn grow() { + let a = BoundingSphere::new(Vec3::ONE, 5.); + let padded = a.grow(1.25); + assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON); + assert!(padded.contains(&a)); + assert!(!a.contains(&padded)); + } + + #[test] + fn shrink() { + let a = BoundingSphere::new(Vec3::ONE, 5.); + let shrunk = a.shrink(0.5); + assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON); + assert!(a.contains(&shrunk)); + assert!(!shrunk.contains(&a)); + } +} diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs new file mode 100644 index 0000000000000..b992fb517ea22 --- /dev/null +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -0,0 +1,549 @@ +//! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). + +use glam::{Mat3, Quat, Vec2, Vec3}; + +use crate::{ + bounding::{Bounded2d, BoundingCircle}, + primitives::{ + BoxedPolyline3d, Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, + Plane3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, + }, +}; + +use super::{Aabb3d, Bounded3d, BoundingSphere}; + +impl Bounded3d for Sphere { + fn aabb_3d(&self, translation: Vec3, _rotation: Quat) -> Aabb3d { + Aabb3d::new(translation, Vec3::splat(self.radius)) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, self.radius) + } +} + +impl Bounded3d for Plane3d { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + let normal = rotation * *self.normal; + let facing_x = normal == Vec3::X || normal == Vec3::NEG_X; + let facing_y = normal == Vec3::Y || normal == Vec3::NEG_Y; + let facing_z = normal == Vec3::Z || normal == Vec3::NEG_Z; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let half_width = if facing_x { 0.0 } else { f32::MAX / 2.0 }; + let half_height = if facing_y { 0.0 } else { f32::MAX / 2.0 }; + let half_depth = if facing_z { 0.0 } else { f32::MAX / 2.0 }; + let half_size = Vec3::new(half_width, half_height, half_depth); + + Aabb3d::new(translation, half_size) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, f32::MAX / 2.0) + } +} + +impl Bounded3d for Line3d { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + let direction = rotation * *self.direction; + + // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations + // like growing or shrinking the AABB without breaking things. + let max = f32::MAX / 2.0; + let half_width = if direction.x == 0.0 { 0.0 } else { max }; + let half_height = if direction.y == 0.0 { 0.0 } else { max }; + let half_depth = if direction.z == 0.0 { 0.0 } else { max }; + let half_size = Vec3::new(half_width, half_height, half_depth); + + Aabb3d::new(translation, half_size) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, f32::MAX / 2.0) + } +} + +impl Bounded3d for Segment3d { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Rotate the segment by `rotation` + let direction = rotation * *self.direction; + let half_size = (self.half_length * direction).abs(); + + Aabb3d::new(translation, half_size) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, self.half_length) + } +} + +impl Bounded3d for Polyline3d { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + Aabb3d::from_point_cloud(translation, rotation, &self.vertices) + } + + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + BoundingSphere::from_point_cloud(translation, rotation, &self.vertices) + } +} + +impl Bounded3d for BoxedPolyline3d { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + Aabb3d::from_point_cloud(translation, rotation, &self.vertices) + } + + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + BoundingSphere::from_point_cloud(translation, rotation, &self.vertices) + } +} + +impl Bounded3d for Cuboid { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Compute the AABB of the rotated cuboid by transforming the half-size + // by an absolute rotation matrix. + let rot_mat = Mat3::from_quat(rotation); + let abs_rot_mat = Mat3::from_cols( + rot_mat.x_axis.abs(), + rot_mat.y_axis.abs(), + rot_mat.z_axis.abs(), + ); + let half_size = abs_rot_mat * self.half_size; + + Aabb3d::new(translation, half_size) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere { + center: translation, + sphere: Sphere { + radius: self.half_size.length(), + }, + } + } +} + +impl Bounded3d for Cylinder { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let segment_dir = rotation * Vec3::Y; + let top = segment_dir * self.half_height; + let bottom = -top; + + let e = Vec3::ONE - segment_dir * segment_dir; + let half_size = self.radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + + Aabb3d { + min: translation + (top - half_size).min(bottom - half_size), + max: translation + (top + half_size).max(bottom + half_size), + } + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + let radius = self.radius.hypot(self.half_height); + BoundingSphere::new(translation, radius) + } +} + +impl Bounded3d for Capsule { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Get the line segment between the hemispheres of the rotated capsule + let segment = Segment3d { + // Multiplying a normalized vector (Vec3::Y) with a rotation returns a normalized vector. + direction: Direction3d::new_unchecked(rotation * Vec3::Y), + half_length: self.half_length, + }; + let (a, b) = (segment.point1(), segment.point2()); + + // Expand the line segment by the capsule radius to get the capsule half-extents + let min = a.min(b) - Vec3::splat(self.radius); + let max = a.max(b) + Vec3::splat(self.radius); + + Aabb3d { + min: min + translation, + max: max + translation, + } + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, self.radius + self.half_length) + } +} + +impl Bounded3d for Cone { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let top = rotation * Vec3::Y * 0.5 * self.height; + let bottom = -top; + let segment = bottom - top; + + let e = 1.0 - segment * segment / segment.length_squared(); + let half_extents = Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + + Aabb3d { + min: translation + top.min(bottom - self.radius * half_extents), + max: translation + top.max(bottom + self.radius * half_extents), + } + } + + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + // Get the triangular cross-section of the cone. + let half_height = 0.5 * self.height; + let triangle = Triangle2d::new( + half_height * Vec2::Y, + Vec2::new(-self.radius, -half_height), + Vec2::new(self.radius, -half_height), + ); + + // Because of circular symmetry, we can use the bounding circle of the triangle + // for the bounding sphere of the cone. + let BoundingCircle { circle, center } = triangle.bounding_circle(Vec2::ZERO, 0.0); + + BoundingSphere::new(rotation * center.extend(0.0) + translation, circle.radius) + } +} + +impl Bounded3d for ConicalFrustum { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Reference: http://iquilezles.org/articles/diskbbox/ + + let top = rotation * Vec3::Y * 0.5 * self.height; + let bottom = -top; + let segment = bottom - top; + + let e = 1.0 - segment * segment / segment.length_squared(); + let half_extents = Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + + Aabb3d { + min: translation + + (top - self.radius_top * half_extents) + .min(bottom - self.radius_bottom * half_extents), + max: translation + + (top + self.radius_top * half_extents) + .max(bottom + self.radius_bottom * half_extents), + } + } + + fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + let half_height = 0.5 * self.height; + + // To compute the bounding sphere, we'll get the center and radius of the circumcircle + // passing through all four vertices of the trapezoidal cross-section of the conical frustum. + // + // If the circumcenter is inside the trapezoid, we can use that for the bounding sphere. + // Otherwise, we clamp it to the longer parallel side to get a more tightly fitting bounding sphere. + // + // The circumcenter is at the intersection of the bisectors perpendicular to the sides. + // For the isosceles trapezoid, the X coordinate is zero at the center, so a single bisector is enough. + // + // A + // *-------* + // / | \ + // / | \ + // AB / \ | / \ + // / \ | / \ + // / C \ + // *-------------------* + // B + + let a = Vec2::new(-self.radius_top, half_height); + let b = Vec2::new(-self.radius_bottom, -half_height); + let ab = a - b; + let ab_midpoint = b + 0.5 * ab; + let bisector = ab.perp(); + + // Compute intersection between bisector and vertical line at x = 0. + // + // x = ab_midpoint.x + t * bisector.x = 0 + // y = ab_midpoint.y + t * bisector.y = ? + // + // Because ab_midpoint.y = 0 for our conical frustum, we get: + // y = t * bisector.y + // + // Solve x for t: + // t = -ab_midpoint.x / bisector.x + // + // Substitute t to solve for y: + // y = -ab_midpoint.x / bisector.x * bisector.y + let circumcenter_y = -ab_midpoint.x / bisector.x * bisector.y; + + // If the circumcenter is outside the trapezoid, the bounding circle is too large. + // In those cases, we clamp it to the longer parallel side. + let (center, radius) = if circumcenter_y <= -half_height { + (Vec2::new(0.0, -half_height), self.radius_bottom) + } else if circumcenter_y >= half_height { + (Vec2::new(0.0, half_height), self.radius_top) + } else { + let circumcenter = Vec2::new(0.0, circumcenter_y); + // We can use the distance from an arbitrary vertex because they all lie on the circumcircle. + (circumcenter, a.distance(circumcenter)) + }; + + BoundingSphere::new(translation + rotation * center.extend(0.0), radius) + } +} + +impl Bounded3d for Torus { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + // Compute the AABB of a flat disc with the major radius of the torus. + // Reference: http://iquilezles.org/articles/diskbbox/ + let normal = rotation * Vec3::Y; + let e = 1.0 - normal * normal; + let disc_half_size = self.major_radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + + // Expand the disc by the minor radius to get the torus half-size + let half_size = disc_half_size + Vec3::splat(self.minor_radius); + + Aabb3d::new(translation, half_size) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, self.outer_radius()) + } +} + +#[cfg(test)] +mod tests { + use glam::{Quat, Vec3}; + + use crate::{ + bounding::Bounded3d, + primitives::{ + Capsule, Cone, ConicalFrustum, Cuboid, Cylinder, Direction3d, Line3d, Plane3d, + Polyline3d, Segment3d, Sphere, Torus, + }, + }; + + #[test] + fn sphere() { + let sphere = Sphere { radius: 1.0 }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = sphere.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3::new(3.0, 2.0, 1.0)); + + let bounding_sphere = sphere.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.0); + } + + #[test] + fn plane() { + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb1 = Plane3d::new(Vec3::X).aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb1.min, Vec3::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0)); + assert_eq!(aabb1.max, Vec3::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0)); + + let aabb2 = Plane3d::new(Vec3::Y).aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb2.min, Vec3::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0)); + assert_eq!(aabb2.max, Vec3::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0)); + + let aabb3 = Plane3d::new(Vec3::Z).aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb3.min, Vec3::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0)); + assert_eq!(aabb3.max, Vec3::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0)); + + let aabb4 = Plane3d::new(Vec3::ONE).aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb4.min, Vec3::splat(-f32::MAX / 2.0)); + assert_eq!(aabb4.max, Vec3::splat(f32::MAX / 2.0)); + + let bounding_sphere = Plane3d::new(Vec3::Y).bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); + } + + #[test] + fn line() { + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb1 = Line3d { + direction: Direction3d::Y, + } + .aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb1.min, Vec3::new(2.0, -f32::MAX / 2.0, 0.0)); + assert_eq!(aabb1.max, Vec3::new(2.0, f32::MAX / 2.0, 0.0)); + + let aabb2 = Line3d { + direction: Direction3d::X, + } + .aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb2.min, Vec3::new(-f32::MAX / 2.0, 1.0, 0.0)); + assert_eq!(aabb2.max, Vec3::new(f32::MAX / 2.0, 1.0, 0.0)); + + let aabb3 = Line3d { + direction: Direction3d::Z, + } + .aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb3.min, Vec3::new(2.0, 1.0, -f32::MAX / 2.0)); + assert_eq!(aabb3.max, Vec3::new(2.0, 1.0, f32::MAX / 2.0)); + + let aabb4 = Line3d { + direction: Direction3d::from_xyz(1.0, 1.0, 1.0).unwrap(), + } + .aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb4.min, Vec3::splat(-f32::MAX / 2.0)); + assert_eq!(aabb4.max, Vec3::splat(f32::MAX / 2.0)); + + let bounding_sphere = Line3d { + direction: Direction3d::Y, + } + .bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); + } + + #[test] + fn segment() { + let translation = Vec3::new(2.0, 1.0, 0.0); + let segment = + Segment3d::from_points(Vec3::new(-1.0, -0.5, 0.0), Vec3::new(1.0, 0.5, 0.0)).0; + + let aabb = segment.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(1.0, 0.5, 0.0)); + assert_eq!(aabb.max, Vec3::new(3.0, 1.5, 0.0)); + + let bounding_sphere = segment.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5)); + } + + #[test] + fn polyline() { + let polyline = Polyline3d::<4>::new([ + Vec3::ONE, + Vec3::new(-1.0, 1.0, 1.0), + Vec3::NEG_ONE, + Vec3::new(1.0, -1.0, -1.0), + ]); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = polyline.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3::new(3.0, 2.0, 1.0)); + + let bounding_sphere = polyline.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(1.0).hypot(1.0)); + } + + #[test] + fn cuboid() { + let cuboid = Cuboid::new(2.0, 1.0, 1.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cuboid.aabb_3d( + translation, + Quat::from_rotation_z(std::f32::consts::FRAC_PI_4), + ); + let expected_half_size = Vec3::new(1.0606601, 1.0606601, 0.5); + assert_eq!(aabb.min, translation - expected_half_size); + assert_eq!(aabb.max, translation + expected_half_size); + + let bounding_sphere = cuboid.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5).hypot(0.5)); + } + + #[test] + fn cylinder() { + let cylinder = Cylinder::new(0.5, 2.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cylinder.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, translation - Vec3::new(0.5, 1.0, 0.5)); + assert_eq!(aabb.max, translation + Vec3::new(0.5, 1.0, 0.5)); + + let bounding_sphere = cylinder.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5)); + } + + #[test] + fn capsule() { + let capsule = Capsule::new(0.5, 2.0); + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = capsule.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, translation - Vec3::new(0.5, 1.5, 0.5)); + assert_eq!(aabb.max, translation + Vec3::new(0.5, 1.5, 0.5)); + + let bounding_sphere = capsule.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.5); + } + + #[test] + fn cone() { + let cone = Cone { + radius: 1.0, + height: 2.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = cone.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3::new(3.0, 2.0, 1.0)); + + let bounding_sphere = cone.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation + Vec3::NEG_Y * 0.25); + assert_eq!(bounding_sphere.radius(), 1.25); + } + + #[test] + fn conical_frustum() { + let conical_frustum = ConicalFrustum { + radius_top: 0.5, + radius_bottom: 1.0, + height: 2.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = conical_frustum.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(1.0, 0.0, -1.0)); + assert_eq!(aabb.max, Vec3::new(3.0, 2.0, 1.0)); + + let bounding_sphere = conical_frustum.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation + Vec3::NEG_Y * 0.1875); + assert_eq!(bounding_sphere.radius(), 1.2884705); + } + + #[test] + fn wide_conical_frustum() { + let conical_frustum = ConicalFrustum { + radius_top: 0.5, + radius_bottom: 5.0, + height: 1.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = conical_frustum.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(-3.0, 0.5, -5.0)); + assert_eq!(aabb.max, Vec3::new(7.0, 1.5, 5.0)); + + // For wide conical frusta like this, the circumcenter can be outside the frustum, + // so the center and radius should be clamped to the longest side. + let bounding_sphere = conical_frustum.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation + Vec3::NEG_Y * 0.5); + assert_eq!(bounding_sphere.radius(), 5.0); + } + + #[test] + fn torus() { + let torus = Torus { + minor_radius: 0.5, + major_radius: 1.0, + }; + let translation = Vec3::new(2.0, 1.0, 0.0); + + let aabb = torus.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3::new(0.5, 0.5, -1.5)); + assert_eq!(aabb.max, Vec3::new(3.5, 1.5, 1.5)); + + let bounding_sphere = torus.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation); + assert_eq!(bounding_sphere.radius(), 1.5); + } +} diff --git a/crates/bevy_math/src/bounding/mod.rs b/crates/bevy_math/src/bounding/mod.rs new file mode 100644 index 0000000000000..7a07626fddba7 --- /dev/null +++ b/crates/bevy_math/src/bounding/mod.rs @@ -0,0 +1,61 @@ +//! This module contains traits and implements for working with bounding shapes +//! +//! There are four traits used: +//! - [`BoundingVolume`] is a generic abstraction for any bounding volume +//! - [`IntersectsVolume`] abstracts intersection tests against a [`BoundingVolume`] +//! - [`Bounded2d`]/[`Bounded3d`] are abstractions for shapes to generate [`BoundingVolume`]s + +/// A trait that generalizes different bounding volumes. +/// Bounding volumes are simplified shapes that are used to get simpler ways to check for +/// overlapping elements or finding intersections. +/// +/// This trait supports both 2D and 3D bounding shapes. +pub trait BoundingVolume { + /// The position type used for the volume. This should be `Vec2` for 2D and `Vec3` for 3D. + type Position: Clone + Copy + PartialEq; + /// The type used for the size of the bounding volume. Usually a half size. For example an + /// `f32` radius for a circle, or a `Vec3` with half sizes for x, y and z for a 3D axis-aligned + /// bounding box + type HalfSize; + + /// Returns the center of the bounding volume. + fn center(&self) -> Self::Position; + + /// Returns the half size of the bounding volume. + fn half_size(&self) -> Self::HalfSize; + + /// Computes the visible surface area of the bounding volume. + /// This method can be useful to make decisions about merging bounding volumes, + /// using a Surface Area Heuristic. + /// + /// For 2D shapes this would simply be the area of the shape. + /// For 3D shapes this would usually be half the area of the shape. + fn visible_area(&self) -> f32; + + /// Checks if this bounding volume contains another one. + fn contains(&self, other: &Self) -> bool; + + /// Computes the smallest bounding volume that contains both `self` and `other`. + fn merge(&self, other: &Self) -> Self; + + /// Increase the size of the bounding volume in each direction by the given amount + fn grow(&self, amount: Self::HalfSize) -> Self; + + /// Decrease the size of the bounding volume in each direction by the given amount + fn shrink(&self, amount: Self::HalfSize) -> Self; +} + +/// A trait that generalizes intersection tests against a volume. +/// Intersection tests can be used for a variety of tasks, for example: +/// - Raycasting +/// - Testing for overlap +/// - Checking if an object is within the view frustum of a camera +pub trait IntersectsVolume { + /// Check if a volume intersects with this intersection test + fn intersects(&self, volume: &Volume) -> bool; +} + +mod bounded2d; +pub use bounded2d::*; +mod bounded3d; +pub use bounded3d::*; diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 9f4fc365f7799..249bd517c1568 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -431,7 +431,10 @@ impl CubicSegment { } /// A collection of [`CubicSegment`]s chained into a curve. -#[derive(Clone, Debug, Default, PartialEq)] +/// +/// Use any struct that implements the [`CubicGenerator`] trait to create a new curve, such as +/// [`CubicBezier`]. +#[derive(Clone, Debug, PartialEq)] pub struct CubicCurve { segments: Vec>, } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index c6fb416d7eacf..0fd8717964f4b 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -8,6 +8,7 @@ mod affine3; mod aspect_ratio; +pub mod bounding; pub mod cubic_splines; pub mod primitives; mod ray; @@ -26,7 +27,8 @@ pub mod prelude { CubicBSpline, CubicBezier, CubicCardinalSpline, CubicGenerator, CubicHermite, CubicSegment, }, - primitives, BVec2, BVec3, BVec4, EulerRot, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, + primitives::*, + BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray2d, Ray3d, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, }; diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index fbb7811cfbb2e..39d6b740cee11 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -7,25 +7,45 @@ use crate::Vec2; pub struct Direction2d(Vec2); impl Direction2d { + /// A unit vector pointing along the positive X axis. + pub const X: Self = Self(Vec2::X); + /// A unit vector pointing along the positive Y axis. + pub const Y: Self = Self(Vec2::Y); + /// A unit vector pointing along the negative X axis. + pub const NEG_X: Self = Self(Vec2::NEG_X); + /// A unit vector pointing along the negative Y axis. + pub const NEG_Y: Self = Self(Vec2::NEG_Y); + /// Create a direction from a finite, nonzero [`Vec2`]. /// /// 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: Vec2) -> Result { - value.try_normalize().map(Self).map_or_else( - || { - if value.is_nan() { - Err(InvalidDirectionError::NaN) - } else if !value.is_finite() { - // If the direction is non-finite but also not NaN, it must be infinite - Err(InvalidDirectionError::Infinite) - } else { - // If the direction is invalid but neither NaN nor infinite, it must be zero - Err(InvalidDirectionError::Zero) - } - }, - Ok, - ) + Self::new_and_length(value).map(|(dir, _)| dir) + } + + /// Create a [`Direction2d`] from a [`Vec2`] that is already normalized. + /// + /// # Warning + /// + /// `value` must be normalized, i.e it's length must be `1.0`. + pub fn new_unchecked(value: Vec2) -> Self { + debug_assert!(value.is_normalized()); + + Self(value) + } + + /// Create a direction from a finite, nonzero [`Vec2`], 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: Vec2) -> 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` and `y` components. @@ -35,12 +55,6 @@ impl Direction2d { pub fn from_xy(x: f32, y: f32) -> Result { Self::new(Vec2::new(x, y)) } - - /// Create a direction from a [`Vec2`] that is already normalized. - pub fn from_normalized(value: Vec2) -> Self { - debug_assert!(value.is_normalized()); - Self(value) - } } impl TryFrom for Direction2d { @@ -58,6 +72,13 @@ impl std::ops::Deref for Direction2d { } } +impl std::ops::Neg for Direction2d { + type Output = Self; + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + /// A circle primitive #[derive(Clone, Copy, Debug)] pub struct Circle { @@ -69,21 +90,45 @@ impl Primitive2d for Circle {} /// An ellipse primitive #[derive(Clone, Copy, Debug)] pub struct Ellipse { - /// The half "width" of the ellipse - pub half_width: f32, - /// The half "height" of the ellipse - pub half_height: f32, + /// Half of the width and height of the ellipse. + /// + /// This corresponds to the two perpendicular radii defining the ellipse. + pub half_size: Vec2, } impl Primitive2d for Ellipse {} impl Ellipse { - /// Create a new `Ellipse` from a "width" and a "height" - pub fn new(width: f32, height: f32) -> Self { + /// Create a new `Ellipse` from half of its width and height. + /// + /// This corresponds to the two perpendicular radii defining the ellipse. + #[inline] + pub const fn new(half_width: f32, half_height: f32) -> Self { + Self { + half_size: Vec2::new(half_width, half_height), + } + } + + /// Create a new `Ellipse` from a given full size. + /// + /// `size.x` is the diameter along the X axis, and `size.y` is the diameter along the Y axis. + #[inline] + pub fn from_size(size: Vec2) -> Self { Self { - half_width: width / 2.0, - half_height: height / 2.0, + half_size: size / 2.0, } } + + /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. + #[inline] + pub fn semi_major(self) -> f32 { + self.half_size.max_element() + } + + /// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse. + #[inline] + pub fn semi_minor(self) -> f32 { + self.half_size.min_element() + } } /// An unbounded plane in 2D space. It forms a separating surface through the origin, @@ -147,8 +192,10 @@ impl Segment2d { pub fn from_points(point1: Vec2, point2: Vec2) -> (Self, Vec2) { let diff = point2 - point1; let length = diff.length(); + ( - Self::new(Direction2d::from_normalized(diff / length), length), + // We are dividing by the length here, so the vector is normalized. + Self::new(Direction2d::new_unchecked(diff / length), length), (point1 + point2) / 2., ) } @@ -249,6 +296,41 @@ impl Triangle2d { } } + /// Compute the circle passing through all three vertices of the triangle. + /// The vector in the returned tuple is the circumcenter. + pub fn circumcircle(&self) -> (Circle, Vec2) { + // We treat the triangle as translated so that vertex A is at the origin. This simplifies calculations. + // + // A = (0, 0) + // * + // / \ + // / \ + // / \ + // / \ + // / U \ + // / \ + // *-------------* + // B C + + let a = self.vertices[0]; + let (b, c) = (self.vertices[1] - a, self.vertices[2] - a); + let b_length_sq = b.length_squared(); + let c_length_sq = c.length_squared(); + + // Reference: https://en.wikipedia.org/wiki/Circumcircle#Cartesian_coordinates_2 + let inv_d = (2.0 * (b.x * c.y - b.y * c.x)).recip(); + let ux = inv_d * (c.y * b_length_sq - b.y * c_length_sq); + let uy = inv_d * (b.x * c_length_sq - c.x * b_length_sq); + let u = Vec2::new(ux, uy); + + // Compute true circumcenter and circumradius, adding the tip coordinate so that + // A is translated back to its actual coordinate. + let center = u + a; + let radius = u.length(); + + (Circle { radius }, center) + } + /// Reverse the [`WindingOrder`] of the triangle /// by swapping the second and third vertices pub fn reverse(&mut self) { @@ -260,12 +342,9 @@ impl Triangle2d { #[doc(alias = "Quad")] #[derive(Clone, Copy, Debug)] pub struct Rectangle { - /// The half width of the rectangle - pub half_width: f32, - /// The half height of the rectangle - pub half_height: f32, + /// Half of the width and height of the rectangle + pub half_size: Vec2, } -impl Primitive2d for Rectangle {} impl Rectangle { /// Create a rectangle from a full width and height @@ -276,8 +355,7 @@ impl Rectangle { /// Create a rectangle from a given full size pub fn from_size(size: Vec2) -> Self { Self { - half_width: size.x / 2., - half_height: size.y / 2., + half_size: size / 2., } } } @@ -337,7 +415,7 @@ impl BoxedPolygon { } } -/// A polygon where all vertices lie on a circle, equally far apart +/// A polygon where all vertices lie on a circle, equally far apart. #[derive(Clone, Copy, Debug)] pub struct RegularPolygon { /// The circumcircle on which all vertices lie @@ -363,6 +441,22 @@ impl RegularPolygon { sides, } } + + /// Returns an iterator over the vertices of the regular polygon, + /// rotated counterclockwise by the given angle in radians. + /// + /// With a rotation of 0, a vertex will be placed at the top `(0.0, circumradius)`. + pub fn vertices(self, rotation: f32) -> impl IntoIterator { + // Add pi/2 so that the polygon has a vertex at the top (sin is 1.0 and cos is 0.0) + let start_angle = rotation + std::f32::consts::FRAC_PI_2; + let step = std::f32::consts::TAU / self.sides as f32; + + (0..self.sides).map(move |i| { + let theta = start_angle + i as f32 * step; + let (sin, cos) = theta.sin_cos(); + Vec2::new(cos, sin) * self.circumcircle.radius + }) + } } #[cfg(test)] @@ -371,10 +465,7 @@ mod tests { #[test] fn direction_creation() { - assert_eq!( - Direction2d::new(Vec2::X * 12.5), - Ok(Direction2d::from_normalized(Vec2::X)) - ); + assert_eq!(Direction2d::new(Vec2::X * 12.5), Ok(Direction2d::X)); assert_eq!( Direction2d::new(Vec2::new(0.0, 0.0)), Err(InvalidDirectionError::Zero) @@ -391,6 +482,10 @@ mod tests { Direction2d::new(Vec2::new(f32::NAN, 0.0)), Err(InvalidDirectionError::NaN) ); + assert_eq!( + Direction2d::new_and_length(Vec2::X * 6.5), + Ok((Direction2d::X, 6.5)) + ); } #[test] @@ -421,4 +516,37 @@ mod tests { ); assert_eq!(invalid_triangle.winding_order(), WindingOrder::Invalid); } + + #[test] + fn triangle_circumcenter() { + let triangle = Triangle2d::new( + Vec2::new(10.0, 2.0), + Vec2::new(-5.0, -3.0), + Vec2::new(2.0, -1.0), + ); + let (Circle { radius }, circumcenter) = triangle.circumcircle(); + + // Calculated with external calculator + assert_eq!(radius, 98.34887); + assert_eq!(circumcenter, Vec2::new(-28.5, 92.5)); + } + + #[test] + fn regular_polygon_vertices() { + let polygon = RegularPolygon::new(1.0, 4); + + // Regular polygons have a vertex at the top by default + let mut vertices = polygon.vertices(0.0).into_iter(); + assert!((vertices.next().unwrap() - Vec2::Y).length() < 1e-7); + + // Rotate by 45 degrees, forming an axis-aligned square + let mut rotated_vertices = polygon.vertices(std::f32::consts::FRAC_PI_4).into_iter(); + + // Distance from the origin to the middle of a side, derived using Pythagorean theorem + let side_sistance = std::f32::consts::FRAC_1_SQRT_2; + assert!( + (rotated_vertices.next().unwrap() - Vec2::new(-side_sistance, side_sistance)).length() + < 1e-7, + ); + } } diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 0177d03fc3620..3809271c32de5 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -7,25 +7,49 @@ use crate::Vec3; pub struct Direction3d(Vec3); impl Direction3d { + /// A unit vector pointing along the positive X axis. + pub const X: Self = Self(Vec3::X); + /// A unit vector pointing along the positive Y axis. + pub const Y: Self = Self(Vec3::Y); + /// A unit vector pointing along the positive Z axis. + pub const Z: Self = Self(Vec3::Z); + /// A unit vector pointing along the negative X axis. + pub const NEG_X: Self = Self(Vec3::NEG_X); + /// A unit vector pointing along the negative Y axis. + pub const NEG_Y: Self = Self(Vec3::NEG_Y); + /// A unit vector pointing along the negative Z axis. + pub const NEG_Z: Self = Self(Vec3::NEG_Z); + /// Create a direction from a finite, nonzero [`Vec3`]. /// /// 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: Vec3) -> Result { - value.try_normalize().map(Self).map_or_else( - || { - if value.is_nan() { - Err(InvalidDirectionError::NaN) - } else if !value.is_finite() { - // If the direction is non-finite but also not NaN, it must be infinite - Err(InvalidDirectionError::Infinite) - } else { - // If the direction is invalid but neither NaN nor infinite, it must be zero - Err(InvalidDirectionError::Zero) - } - }, - Ok, - ) + Self::new_and_length(value).map(|(dir, _)| dir) + } + + /// Create a [`Direction3d`] from a [`Vec3`] that is already normalized. + /// + /// # Warning + /// + /// `value` must be normalized, i.e it's length must be `1.0`. + pub fn new_unchecked(value: Vec3) -> Self { + debug_assert!(value.is_normalized()); + + Self(value) + } + + /// Create a direction from a finite, nonzero [`Vec3`], 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: Vec3) -> 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`, and `z` components. @@ -35,12 +59,6 @@ impl Direction3d { pub fn from_xyz(x: f32, y: f32, z: f32) -> Result { Self::new(Vec3::new(x, y, z)) } - - /// Create a direction from a [`Vec3`] that is already normalized. - pub fn from_normalized(value: Vec3) -> Self { - debug_assert!(value.is_normalized()); - Self(value) - } } impl TryFrom for Direction3d { @@ -58,6 +76,13 @@ impl std::ops::Deref for Direction3d { } } +impl std::ops::Neg for Direction3d { + type Output = Self; + fn neg(self) -> Self::Output { + Self(-self.0) + } +} + /// A sphere primitive #[derive(Clone, Copy, Debug)] pub struct Sphere { @@ -126,8 +151,10 @@ impl Segment3d { pub fn from_points(point1: Vec3, point2: Vec3) -> (Self, Vec3) { let diff = point2 - point1; let length = diff.length(); + ( - Self::new(Direction3d::from_normalized(diff / length), length), + // We are dividing by the length here, so the vector is normalized. + Self::new(Direction3d::new_unchecked(diff / length), length), (point1 + point2) / 2., ) } @@ -202,7 +229,7 @@ impl BoxedPolyline3d { #[derive(Clone, Copy, Debug)] pub struct Cuboid { /// Half of the width, height and depth of the cuboid - pub half_extents: Vec3, + pub half_size: Vec3, } impl Primitive3d for Cuboid {} @@ -215,7 +242,7 @@ impl Cuboid { /// Create a cuboid from a given full size pub fn from_size(size: Vec3) -> Self { Self { - half_extents: size / 2., + half_size: size / 2., } } } @@ -384,10 +411,7 @@ mod test { #[test] fn direction_creation() { - assert_eq!( - Direction3d::new(Vec3::X * 12.5), - Ok(Direction3d::from_normalized(Vec3::X)) - ); + assert_eq!(Direction3d::new(Vec3::X * 12.5), Ok(Direction3d::X)); assert_eq!( Direction3d::new(Vec3::new(0.0, 0.0, 0.0)), Err(InvalidDirectionError::Zero) @@ -404,5 +428,9 @@ mod test { Direction3d::new(Vec3::new(f32::NAN, 0.0, 0.0)), Err(InvalidDirectionError::NaN) ); + assert_eq!( + Direction3d::new_and_length(Vec3::X * 6.5), + Ok((Direction3d::X, 6.5)) + ); } } diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_math/src/primitives/mod.rs index d66b161172636..a6567e6330c33 100644 --- a/crates/bevy_math/src/primitives/mod.rs +++ b/crates/bevy_math/src/primitives/mod.rs @@ -24,6 +24,21 @@ pub enum InvalidDirectionError { NaN, } +impl InvalidDirectionError { + /// Creates an [`InvalidDirectionError`] from the length of an invalid direction vector. + pub fn from_length(length: f32) -> Self { + if length.is_nan() { + InvalidDirectionError::NaN + } else if !length.is_finite() { + // If the direction is non-finite but also not NaN, it must be infinite + InvalidDirectionError::Infinite + } else { + // If the direction is invalid but neither NaN nor infinite, it must be zero + InvalidDirectionError::Zero + } + } +} + impl std::fmt::Display for InvalidDirectionError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( diff --git a/crates/bevy_math/src/rects/irect.rs b/crates/bevy_math/src/rects/irect.rs index b8a1b21a792a4..8f5cad3161ae6 100644 --- a/crates/bevy_math/src/rects/irect.rs +++ b/crates/bevy_math/src/rects/irect.rs @@ -26,7 +26,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::IRect; /// let r = IRect::new(0, 4, 10, 6); // w=10 h=2 /// let r = IRect::new(2, 3, 5, -1); // w=3 h=4 @@ -43,7 +43,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// // Unit rect from [0,0] to [1,1] /// let r = IRect::from_corners(IVec2::ZERO, IVec2::ONE); // w=1 h=1 @@ -70,7 +70,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::from_center_size(IVec2::ZERO, IVec2::new(3, 2)); // w=2 h=2 /// assert_eq!(r.min, IVec2::splat(-1)); @@ -91,7 +91,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::from_center_half_size(IVec2::ZERO, IVec2::ONE); // w=2 h=2 /// assert_eq!(r.min, IVec2::splat(-1)); @@ -113,7 +113,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::from_corners(IVec2::ZERO, IVec2::new(0, 1)); // w=0 h=1 /// assert!(r.is_empty()); @@ -127,7 +127,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::IRect; /// let r = IRect::new(0, 0, 5, 1); // w=5 h=1 /// assert_eq!(r.width(), 5); @@ -141,7 +141,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::IRect; /// let r = IRect::new(0, 0, 5, 1); // w=5 h=1 /// assert_eq!(r.height(), 1); @@ -155,7 +155,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::new(0, 0, 5, 1); // w=5 h=1 /// assert_eq!(r.size(), IVec2::new(5, 1)); @@ -173,7 +173,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::new(0, 0, 4, 3); // w=4 h=3 /// assert_eq!(r.half_size(), IVec2::new(2, 1)); @@ -191,7 +191,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::new(0, 0, 5, 2); // w=5 h=2 /// assert_eq!(r.center(), IVec2::new(2, 1)); @@ -205,7 +205,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::IRect; /// let r = IRect::new(0, 0, 5, 1); // w=5 h=1 /// assert!(r.contains(r.center())); @@ -223,7 +223,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r1 = IRect::new(0, 0, 5, 1); // w=5 h=1 /// let r2 = IRect::new(1, -1, 3, 3); // w=2 h=4 @@ -246,7 +246,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::new(0, 0, 5, 1); // w=5 h=1 /// let u = r.union_point(IVec2::new(3, 6)); @@ -269,7 +269,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r1 = IRect::new(0, 0, 5, 1); // w=5 h=1 /// let r2 = IRect::new(1, -1, 3, 3); // w=2 h=4 @@ -297,7 +297,7 @@ impl IRect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{IRect, IVec2}; /// let r = IRect::new(0, 0, 5, 1); // w=5 h=1 /// let r2 = r.inset(3); // w=11 h=7 diff --git a/crates/bevy_math/src/rects/rect.rs b/crates/bevy_math/src/rects/rect.rs index d3c5aa5bb563f..91377302ec661 100644 --- a/crates/bevy_math/src/rects/rect.rs +++ b/crates/bevy_math/src/rects/rect.rs @@ -26,7 +26,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::Rect; /// let r = Rect::new(0., 4., 10., 6.); // w=10 h=2 /// let r = Rect::new(2., 3., 5., -1.); // w=3 h=4 @@ -43,7 +43,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// // Unit rect from [0,0] to [1,1] /// let r = Rect::from_corners(Vec2::ZERO, Vec2::ONE); // w=1 h=1 @@ -66,7 +66,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::from_center_size(Vec2::ZERO, Vec2::ONE); // w=1 h=1 /// assert!(r.min.abs_diff_eq(Vec2::splat(-0.5), 1e-5)); @@ -87,7 +87,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::from_center_half_size(Vec2::ZERO, Vec2::ONE); // w=2 h=2 /// assert!(r.min.abs_diff_eq(Vec2::splat(-1.), 1e-5)); @@ -109,7 +109,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::from_corners(Vec2::ZERO, Vec2::new(0., 1.)); // w=0 h=1 /// assert!(r.is_empty()); @@ -123,7 +123,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::Rect; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// assert!((r.width() - 5.).abs() <= 1e-5); @@ -137,7 +137,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::Rect; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// assert!((r.height() - 1.).abs() <= 1e-5); @@ -151,7 +151,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// assert!(r.size().abs_diff_eq(Vec2::new(5., 1.), 1e-5)); @@ -165,7 +165,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// assert!(r.half_size().abs_diff_eq(Vec2::new(2.5, 0.5), 1e-5)); @@ -179,7 +179,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// assert!(r.center().abs_diff_eq(Vec2::new(2.5, 0.5), 1e-5)); @@ -193,7 +193,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::Rect; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// assert!(r.contains(r.center())); @@ -211,7 +211,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r1 = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// let r2 = Rect::new(1., -1., 3., 3.); // w=2 h=4 @@ -234,7 +234,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// let u = r.union_point(Vec2::new(3., 6.)); @@ -257,7 +257,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r1 = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// let r2 = Rect::new(1., -1., 3., 3.); // w=2 h=4 @@ -285,7 +285,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::new(0., 0., 5., 1.); // w=5 h=1 /// let r2 = r.inset(3.); // w=11 h=7 @@ -314,7 +314,7 @@ impl Rect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{Rect, Vec2}; /// let r = Rect::new(2., 3., 4., 6.); /// let s = Rect::new(0., 0., 10., 10.); diff --git a/crates/bevy_math/src/rects/urect.rs b/crates/bevy_math/src/rects/urect.rs index 73517de3056e4..56c9482d87e07 100644 --- a/crates/bevy_math/src/rects/urect.rs +++ b/crates/bevy_math/src/rects/urect.rs @@ -26,7 +26,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::URect; /// let r = URect::new(0, 4, 10, 6); // w=10 h=2 /// let r = URect::new(2, 4, 5, 0); // w=3 h=4 @@ -43,7 +43,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// // Unit rect from [0,0] to [1,1] /// let r = URect::from_corners(UVec2::ZERO, UVec2::ONE); // w=1 h=1 @@ -70,7 +70,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::from_center_size(UVec2::ONE, UVec2::splat(2)); // w=2 h=2 /// assert_eq!(r.min, UVec2::splat(0)); @@ -91,7 +91,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::from_center_half_size(UVec2::ONE, UVec2::ONE); // w=2 h=2 /// assert_eq!(r.min, UVec2::splat(0)); @@ -110,7 +110,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::from_corners(UVec2::ZERO, UVec2::new(0, 1)); // w=0 h=1 /// assert!(r.is_empty()); @@ -124,7 +124,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::URect; /// let r = URect::new(0, 0, 5, 1); // w=5 h=1 /// assert_eq!(r.width(), 5); @@ -138,7 +138,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::URect; /// let r = URect::new(0, 0, 5, 1); // w=5 h=1 /// assert_eq!(r.height(), 1); @@ -152,7 +152,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::new(0, 0, 5, 1); // w=5 h=1 /// assert_eq!(r.size(), UVec2::new(5, 1)); @@ -170,7 +170,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::new(0, 0, 4, 2); // w=4 h=2 /// assert_eq!(r.half_size(), UVec2::new(2, 1)); @@ -188,7 +188,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::new(0, 0, 4, 2); // w=4 h=2 /// assert_eq!(r.center(), UVec2::new(2, 1)); @@ -202,7 +202,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::URect; /// let r = URect::new(0, 0, 5, 1); // w=5 h=1 /// assert!(r.contains(r.center())); @@ -220,7 +220,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r1 = URect::new(0, 0, 5, 1); // w=5 h=1 /// let r2 = URect::new(1, 0, 3, 8); // w=2 h=4 @@ -243,7 +243,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::new(0, 0, 5, 1); // w=5 h=1 /// let u = r.union_point(UVec2::new(3, 6)); @@ -266,7 +266,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r1 = URect::new(0, 0, 2, 2); // w=2 h=2 /// let r2 = URect::new(1, 1, 3, 3); // w=2 h=2 @@ -294,7 +294,7 @@ impl URect { /// /// # Examples /// - /// ```rust + /// ``` /// # use bevy_math::{URect, UVec2}; /// let r = URect::new(4, 4, 6, 6); // w=2 h=2 /// let r2 = r.inset(1); // w=4 h=4 diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 451e4e24a07a8..03ff2c30e6d4e 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -15,7 +15,7 @@ license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] [dependencies] -glam = "0.24.1" +glam = "0.25" [[example]] name = "generate" diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index 07f5d53856a63..77945f71fa474 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -210,8 +210,7 @@ pub unsafe fn genTangSpace(geometry: &mut I, fAngularThreshold: f32 let mut index = 0; let iNrFaces = geometry.num_faces(); let mut bRes: bool = false; - let fThresCos: f32 = - ((fAngularThreshold * 3.14159265358979323846f64 as f32 / 180.0f32) as f64).cos() as f32; + let fThresCos = fAngularThreshold.to_radians().cos(); f = 0; while f < iNrFaces { let verts = geometry.num_vertices_of_face(f); diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 433b128812deb..23eeb71e5b33b 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [features] webgl = [] +shader_format_glsl = ["naga_oil/glsl"] pbr_transmission_textures = [] [dependencies] @@ -34,9 +35,17 @@ fixedbitset = "0.4" # direct dependency required for derive macro bytemuck = { version = "1", features = ["derive"] } radsort = "0.1" -naga_oil = "0.11" smallvec = "1.6" thread_local = "1.0" +[target.'cfg(target_arch = "wasm32")'.dependencies] +naga_oil = { version = "0.11" } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# Omit the `glsl` feature in non-WebAssembly by default. +naga_oil = { version = "0.11", default-features = false, features = [ + "test_shader", +] } + [lints] workspace = true diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 851f87bcd7466..6c7829bd1242d 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -11,7 +11,7 @@ use bevy_render::{ view::{InheritedVisibility, ViewVisibility, Visibility, VisibleEntities}, }; use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_utils::HashMap; +use bevy_utils::EntityHashMap; /// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`]. pub type PbrBundle = MaterialMeshBundle; @@ -75,7 +75,7 @@ impl CubemapVisibleEntities { pub struct CascadesVisibleEntities { /// Map of view entity to the visible entities for each cascade frustum. #[reflect(ignore)] - pub entities: HashMap>, + pub entities: EntityHashMap>, } /// A component bundle for [`PointLight`] entities. diff --git a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl index bf837bab55934..de191ce295b6b 100644 --- a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl +++ b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl @@ -4,6 +4,7 @@ pbr_functions, pbr_deferred_functions::pbr_input_from_deferred_gbuffer, pbr_deferred_types::unpack_unorm3x4_plus_unorm_20_, + lighting, mesh_view_bindings::deferred_prepass_texture, } @@ -64,7 +65,15 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); - pbr_input.occlusion = min(pbr_input.occlusion, ssao_multibounce); + pbr_input.diffuse_occlusion = min(pbr_input.diffuse_occlusion, ssao_multibounce); + + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + var perceptual_roughness: f32 = pbr_input.material.perceptual_roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); + // Use SSAO to estimate the specular occlusion. + // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" + pbr_input.specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); #endif // SCREEN_SPACE_AMBIENT_OCCLUSION output_color = pbr_functions::apply_pbr_lighting(pbr_input); diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 48e8b79417644..d7d76b355b518 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -1,13 +1,14 @@ -use crate::{MeshPipeline, MeshViewBindGroup, ScreenSpaceAmbientOcclusionSettings}; +use crate::{ + environment_map::RenderViewEnvironmentMaps, MeshPipeline, MeshViewBindGroup, + ScreenSpaceAmbientOcclusionSettings, ViewLightProbesUniformOffset, +}; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, Handle}; use bevy_core_pipeline::{ - clear_color::ClearColorConfig, core_3d, deferred::{ copy_lighting_id::DeferredLightingIdDepthTexture, DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT, }, - prelude::{Camera3d, ClearColor}, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, tonemapping::{DebandDither, Tonemapping}, }; @@ -16,25 +17,17 @@ use bevy_render::{ extract_component::{ ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, }, - render_asset::RenderAssets, - render_graph::{NodeRunError, RenderGraphContext, ViewNode, ViewNodeRunner}, - render_resource::{ - binding_types::uniform_buffer, Operations, PipelineCache, RenderPassDescriptor, - }, + render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, + render_resource::binding_types::uniform_buffer, + render_resource::*, renderer::{RenderContext, RenderDevice}, - texture::Image, - view::{ViewTarget, ViewUniformOffset}, - Render, RenderSet, -}; - -use bevy_render::{ - render_graph::RenderGraphApp, render_resource::*, texture::BevyDefault, view::ExtractedView, - RenderApp, + texture::BevyDefault, + view::{ExtractedView, ViewTarget, ViewUniformOffset}, + Render, RenderApp, RenderSet, }; use crate::{ - EnvironmentMapLight, MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, - ViewLightsUniformOffset, + MeshPipelineKey, ShadowFilteringMethod, ViewFogUniformOffset, ViewLightsUniformOffset, }; pub struct DeferredPbrLightingPlugin; @@ -153,10 +146,10 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { &'static ViewUniformOffset, &'static ViewLightsUniformOffset, &'static ViewFogUniformOffset, + &'static ViewLightProbesUniformOffset, &'static MeshViewBindGroup, &'static ViewTarget, &'static DeferredLightingIdDepthTexture, - &'static Camera3d, &'static DeferredLightingPipeline, ); @@ -168,10 +161,10 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { view_uniform_offset, view_lights_offset, view_fog_offset, + view_light_probes_offset, mesh_view_bind_group, target, deferred_lighting_id_depth_texture, - camera_3d, deferred_lighting_pipeline, ): QueryItem, world: &World, @@ -201,16 +194,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("deferred_lighting_pass"), - color_attachments: &[Some(target.get_color_attachment(Operations { - load: match camera_3d.clear_color { - ClearColorConfig::Default => { - LoadOp::Clear(world.resource::().0.into()) - } - ClearColorConfig::Custom(color) => LoadOp::Clear(color.into()), - ClearColorConfig::None => LoadOp::Load, - }, - store: StoreOp::Store, - }))], + color_attachments: &[Some(target.get_color_attachment())], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &deferred_lighting_id_depth_texture.texture.default_view, depth_ops: Some(Operations { @@ -231,6 +215,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { view_uniform_offset.offset, view_lights_offset.offset, view_fog_offset.offset, + **view_light_probes_offset, ], ); render_pass.set_bind_group(1, &bind_group_1, &[]); @@ -416,28 +401,27 @@ pub fn prepare_deferred_lighting_pipelines( &ExtractedView, Option<&Tonemapping>, Option<&DebandDither>, - Option<&EnvironmentMapLight>, Option<&ShadowFilteringMethod>, - Option<&ScreenSpaceAmbientOcclusionSettings>, + Has, ( Has, Has, Has, ), + Has, ), With, >, - images: Res>, ) { for ( entity, view, tonemapping, dither, - environment_map, shadow_filter_method, ssao, (normal_prepass, depth_prepass, motion_vector_prepass), + has_environment_maps, ) in &views { let mut view_key = MeshPipelineKey::from_hdr(view.hdr); @@ -480,15 +464,14 @@ pub fn prepare_deferred_lighting_pipelines( } } - if ssao.is_some() { + if ssao { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } - let environment_map_loaded = match environment_map { - Some(environment_map) => environment_map.is_loaded(&images), - None => false, - }; - if environment_map_loaded { + // We don't need to check to see whether the environment map is loaded + // because [`gather_light_probes`] already checked that for us before + // adding the [`RenderViewEnvironmentMaps`] component. + if has_environment_maps { view_key |= MeshPipelineKey::ENVIRONMENT_MAP; } diff --git a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl index d401805eb9d18..7c2696f6c48dc 100644 --- a/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl +++ b/crates/bevy_pbr/src/deferred/pbr_deferred_functions.wgsl @@ -22,18 +22,18 @@ fn deferred_gbuffer_from_pbr_input(in: PbrInput) -> vec4 { // Real time occlusion is applied in the deferred lighting pass. // Deriving luminance via Rec. 709. coefficients // https://en.wikipedia.org/wiki/Rec._709 - let occlusion = dot(in.occlusion, vec3(0.2126, 0.7152, 0.0722)); + let diffuse_occlusion = dot(in.diffuse_occlusion, vec3(0.2126, 0.7152, 0.0722)); #ifdef WEBGL2 // More crunched for webgl so we can also fit depth. var props = deferred_types::pack_unorm3x4_plus_unorm_20_(vec4( in.material.reflectance, in.material.metallic, - occlusion, + diffuse_occlusion, in.frag_coord.z)); #else var props = deferred_types::pack_unorm4x8_(vec4( in.material.reflectance, // could be fewer bits in.material.metallic, // could be fewer bits - occlusion, // is this worth including? + diffuse_occlusion, // is this worth including? 0.0)); // spare #endif // WEBGL2 let flags = deferred_types::deferred_flags_from_mesh_material_flags(in.flags, in.material.flags); @@ -85,7 +85,7 @@ fn pbr_input_from_deferred_gbuffer(frag_coord: vec4, gbuffer: vec4) -> pbr.material.reflectance = props.r; #endif // WEBGL2 pbr.material.metallic = props.g; - pbr.occlusion = vec3(props.b); + pbr.diffuse_occlusion = vec3(props.b); let octahedral_normal = deferred_types::unpack_24bit_normal(gbuffer.a); let N = octahedral_decode(octahedral_normal); diff --git a/crates/bevy_pbr/src/environment_map/environment_map.wgsl b/crates/bevy_pbr/src/environment_map/environment_map.wgsl deleted file mode 100644 index 0b05e352c2d4e..0000000000000 --- a/crates/bevy_pbr/src/environment_map/environment_map.wgsl +++ /dev/null @@ -1,50 +0,0 @@ -#define_import_path bevy_pbr::environment_map - -#import bevy_pbr::mesh_view_bindings as bindings; - -struct EnvironmentMapLight { - diffuse: vec3, - specular: vec3, -}; - -fn environment_map_light( - perceptual_roughness: f32, - roughness: f32, - diffuse_color: vec3, - NdotV: f32, - f_ab: vec2, - N: vec3, - R: vec3, - F0: vec3, -) -> EnvironmentMapLight { - - // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf - // Technically we could use textureNumLevels(environment_map_specular) - 1 here, but we use a uniform - // because textureNumLevels() does not work on WebGL2 - let radiance_level = perceptual_roughness * f32(bindings::lights.environment_map_smallest_specular_mip_level); - let irradiance = textureSampleLevel(bindings::environment_map_diffuse, bindings::environment_map_sampler, vec3(N.xy, -N.z), 0.0).rgb; - let radiance = textureSampleLevel(bindings::environment_map_specular, bindings::environment_map_sampler, vec3(R.xy, -R.z), radiance_level).rgb; - - // No real world material has specular values under 0.02, so we use this range as a - // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. - // See: https://google.github.io/filament/Filament.html#specularocclusion - let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33))); - - // Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf - // Useful reference: https://bruop.github.io/ibl - let Fr = max(vec3(1.0 - roughness), F0) - F0; - let kS = F0 + Fr * pow(1.0 - NdotV, 5.0); - let Ess = f_ab.x + f_ab.y; - let FssEss = kS * Ess * specular_occlusion; - let Ems = 1.0 - Ess; - let Favg = F0 + (1.0 - F0) / 21.0; - let Fms = FssEss * Favg / (1.0 - Ems * Favg); - let FmsEms = Fms * Ems; - let Edss = 1.0 - (FssEss + FmsEms); - let kD = diffuse_color * Edss; - - var out: EnvironmentMapLight; - out.diffuse = (FmsEms + kD) * irradiance; - out.specular = FssEss * radiance; - return out; -} diff --git a/crates/bevy_pbr/src/environment_map/mod.rs b/crates/bevy_pbr/src/environment_map/mod.rs deleted file mode 100644 index 823f264a17923..0000000000000 --- a/crates/bevy_pbr/src/environment_map/mod.rs +++ /dev/null @@ -1,91 +0,0 @@ -use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, Handle}; -use bevy_core_pipeline::prelude::Camera3d; -use bevy_ecs::{prelude::Component, query::With}; -use bevy_reflect::Reflect; -use bevy_render::{ - extract_component::{ExtractComponent, ExtractComponentPlugin}, - render_asset::RenderAssets, - render_resource::{ - binding_types::{sampler, texture_cube}, - *, - }, - texture::{FallbackImageCubemap, Image}, -}; - -pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle = - Handle::weak_from_u128(154476556247605696); - -pub struct EnvironmentMapPlugin; - -impl Plugin for EnvironmentMapPlugin { - fn build(&self, app: &mut App) { - load_internal_asset!( - app, - ENVIRONMENT_MAP_SHADER_HANDLE, - "environment_map.wgsl", - Shader::from_wgsl - ); - - app.register_type::() - .add_plugins(ExtractComponentPlugin::::default()); - } -} - -/// Environment map based ambient lighting representing light from distant scenery. -/// -/// When added to a 3D camera, this component adds indirect light -/// to every point of the scene (including inside, enclosed areas) based on -/// an environment cubemap texture. This is similar to [`crate::AmbientLight`], but -/// higher quality, and is intended for outdoor scenes. -/// -/// The environment map must be prefiltered into a diffuse and specular cubemap based on the -/// [split-sum approximation](https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf). -/// -/// To prefilter your environment map, you can use `KhronosGroup`'s [glTF-IBL-Sampler](https://github.com/KhronosGroup/glTF-IBL-Sampler). -/// The diffuse map uses the Lambertian distribution, and the specular map uses the GGX distribution. -/// -/// `KhronosGroup` also has several prefiltered environment maps that can be found [here](https://github.com/KhronosGroup/glTF-Sample-Environments). -#[derive(Component, Reflect, Clone, ExtractComponent)] -#[extract_component_filter(With)] -pub struct EnvironmentMapLight { - pub diffuse_map: Handle, - pub specular_map: Handle, -} - -impl EnvironmentMapLight { - /// Whether or not all textures necessary to use the environment map - /// have been loaded by the asset server. - pub fn is_loaded(&self, images: &RenderAssets) -> bool { - images.get(&self.diffuse_map).is_some() && images.get(&self.specular_map).is_some() - } -} - -pub fn get_bindings<'a>( - environment_map_light: Option<&EnvironmentMapLight>, - images: &'a RenderAssets, - fallback_image_cubemap: &'a FallbackImageCubemap, -) -> (&'a TextureView, &'a TextureView, &'a Sampler) { - let (diffuse_map, specular_map) = match ( - environment_map_light.and_then(|env_map| images.get(&env_map.diffuse_map)), - environment_map_light.and_then(|env_map| images.get(&env_map.specular_map)), - ) { - (Some(diffuse_map), Some(specular_map)) => { - (&diffuse_map.texture_view, &specular_map.texture_view) - } - _ => ( - &fallback_image_cubemap.texture_view, - &fallback_image_cubemap.texture_view, - ), - }; - - (diffuse_map, specular_map, &fallback_image_cubemap.sampler) -} - -pub fn get_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 3] { - [ - texture_cube(TextureSampleType::Float { filterable: true }), - texture_cube(TextureSampleType::Float { filterable: true }), - sampler(SamplerBindingType::Filtering), - ] -} diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index dacacd5cf3edd..a80a6d5af5503 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -167,6 +167,22 @@ impl Material for ExtendedMaterial { } } + fn alpha_mode(&self) -> crate::AlphaMode { + B::alpha_mode(&self.base) + } + + fn opaque_render_method(&self) -> crate::OpaqueRendererMethod { + B::opaque_render_method(&self.base) + } + + fn depth_bias(&self) -> f32 { + B::depth_bias(&self.base) + } + + fn reads_view_transmission_texture(&self) -> bool { + B::reads_view_transmission_texture(&self.base) + } + fn prepass_vertex_shader() -> ShaderRef { match E::prepass_vertex_shader() { ShaderRef::Default => B::prepass_vertex_shader(), @@ -195,22 +211,6 @@ impl Material for ExtendedMaterial { } } - fn alpha_mode(&self) -> crate::AlphaMode { - B::alpha_mode(&self.base) - } - - fn depth_bias(&self) -> f32 { - B::depth_bias(&self.base) - } - - fn reads_view_transmission_texture(&self) -> bool { - B::reads_view_transmission_texture(&self.base) - } - - fn opaque_render_method(&self) -> crate::OpaqueRendererMethod { - B::opaque_render_method(&self.base) - } - fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index ce104502c8b10..97fd0a0ecc5bd 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -3,10 +3,11 @@ pub mod wireframe; mod alpha; mod bundle; pub mod deferred; -mod environment_map; mod extended_material; mod fog; mod light; +mod light_probe; +mod lightmap; mod material; mod parallax; mod pbr_material; @@ -16,10 +17,11 @@ mod ssao; pub use alpha::*; pub use bundle::*; -pub use environment_map::EnvironmentMapLight; pub use extended_material::*; pub use fog::*; pub use light::*; +pub use light_probe::*; +pub use lightmap::*; pub use material::*; pub use parallax::*; pub use pbr_material::*; @@ -35,9 +37,12 @@ pub mod prelude { DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle, SpotLightBundle, }, - environment_map::EnvironmentMapLight, fog::{FogFalloff, FogSettings}, light::{AmbientLight, DirectionalLight, PointLight, SpotLight}, + light_probe::{ + environment_map::{EnvironmentMapLight, ReflectionProbeBundle}, + LightProbe, + }, material::{Material, MaterialPlugin}, parallax::ParallaxMappingMethod, pbr_material::StandardMaterial, @@ -69,7 +74,6 @@ use bevy_render::{ ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::TransformSystem; -use environment_map::EnvironmentMapPlugin; use crate::deferred::DeferredPbrLightingPlugin; @@ -253,11 +257,12 @@ impl Plugin for PbrPlugin { ..Default::default() }, ScreenSpaceAmbientOcclusionPlugin, - EnvironmentMapPlugin, ExtractResourcePlugin::::default(), FogPlugin, ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), + LightmapPlugin, + LightProbePlugin, )) .configure_sets( PostUpdate, @@ -356,6 +361,15 @@ impl Plugin for PbrPlugin { draw_3d_graph::node::SHADOW_PASS, bevy_core_pipeline::core_3d::graph::node::START_MAIN_PASS, ); + + render_app.ignore_ambiguity( + bevy_render::Render, + bevy_core_pipeline::core_3d::prepare_core_3d_transmission_textures, + bevy_render::batching::batch_and_prepare_render_phase::< + bevy_core_pipeline::core_3d::Transmissive3d, + MeshPipeline, + >, + ); } fn finish(&self, app: &mut App) { diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index c67d0cfe578cd..c4d35164533fa 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -15,8 +15,8 @@ use bevy_render::{ renderer::RenderDevice, view::{InheritedVisibility, RenderLayers, ViewVisibility, VisibleEntities}, }; -use bevy_transform::{components::GlobalTransform, prelude::Transform}; -use bevy_utils::{tracing::warn, HashMap}; +use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_utils::{tracing::warn, EntityHashMap}; use crate::*; @@ -381,7 +381,7 @@ impl From for CascadeShadowConfig { #[reflect(Component)] pub struct Cascades { /// Map from a view to the configuration of each of its [`Cascade`]s. - pub(crate) cascades: HashMap>, + pub(crate) cascades: EntityHashMap>, } #[derive(Clone, Debug, Default, Reflect)] @@ -557,7 +557,7 @@ fn calculate_cascade( /// # use bevy_ecs::system::ResMut; /// # use bevy_pbr::AmbientLight; /// fn setup_ambient_light(mut ambient_light: ResMut) { -/// ambient_light.brightness = 0.3; +/// ambient_light.brightness = 20.0; /// } /// ``` #[derive(Resource, Clone, Debug, ExtractResource, Reflect)] @@ -572,7 +572,7 @@ impl Default for AmbientLight { fn default() -> Self { Self { color: Color::rgb(1.0, 1.0, 1.0), - brightness: 0.05, + brightness: 8.0, } } } diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs new file mode 100644 index 0000000000000..6651d2412044b --- /dev/null +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -0,0 +1,370 @@ +//! Environment maps and reflection probes. +//! +//! An *environment map* consists of a pair of diffuse and specular cubemaps +//! that together reflect the static surrounding area of a region in space. When +//! available, the PBR shader uses these to apply diffuse light and calculate +//! specular reflections. +//! +//! Environment maps come in two flavors, depending on what other components the +//! entities they're attached to have: +//! +//! 1. If attached to a view, they represent the objects located a very far +//! distance from the view, in a similar manner to a skybox. Essentially, these +//! *view environment maps* represent a higher-quality replacement for +//! [`crate::AmbientLight`] for outdoor scenes. The indirect light from such +//! environment maps are added to every point of the scene, including +//! interior enclosed areas. +//! +//! 2. If attached to a [`LightProbe`], environment maps represent the immediate +//! surroundings of a specific location in the scene. These types of +//! environment maps are known as *reflection probes*. +//! [`ReflectionProbeBundle`] is available as a mechanism to conveniently add +//! these to a scene. +//! +//! Typically, environment maps are static (i.e. "baked", calculated ahead of +//! time) and so only reflect fixed static geometry. The environment maps must +//! be pre-filtered into a pair of cubemaps, one for the diffuse component and +//! one for the specular component, according to the [split-sum approximation]. +//! To pre-filter your environment map, you can use the [glTF IBL Sampler] or +//! its [artist-friendly UI]. The diffuse map uses the Lambertian distribution, +//! while the specular map uses the GGX distribution. +//! +//! The Khronos Group has [several pre-filtered environment maps] available for +//! you to use. +//! +//! Currently, reflection probes (i.e. environment maps attached to light +//! probes) use binding arrays (also known as bindless textures) and +//! consequently aren't supported on WebGL2 or WebGPU. Reflection probes are +//! also unsupported if GLSL is in use, due to `naga` limitations. Environment +//! maps attached to views are, however, supported on all platforms. +//! +//! [split-sum approximation]: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf +//! +//! [glTF IBL Sampler]: https://github.com/KhronosGroup/glTF-IBL-Sampler +//! +//! [artist-friendly UI]: https://github.com/pcwalton/gltf-ibl-sampler-egui +//! +//! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments + +use bevy_asset::{AssetId, Handle}; +use bevy_ecs::{ + bundle::Bundle, component::Component, query::QueryItem, system::lifetimeless::Read, +}; +use bevy_reflect::Reflect; +use bevy_render::{ + extract_instances::ExtractInstance, + prelude::SpatialBundle, + render_asset::RenderAssets, + render_resource::{ + binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, + TextureSampleType, TextureView, + }, + renderer::RenderDevice, + settings::WgpuFeatures, + texture::{FallbackImage, Image}, +}; +use bevy_utils::HashMap; +use std::num::NonZeroU32; +use std::ops::Deref; + +use crate::{LightProbe, MAX_VIEW_REFLECTION_PROBES}; + +/// A handle to the environment map helper shader. +pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle = + Handle::weak_from_u128(154476556247605696); + +/// How many texture bindings are used in the fragment shader, *not* counting +/// environment maps. +const STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS: usize = 16; + +/// A pair of cubemap textures that represent the surroundings of a specific +/// area in space. +/// +/// See [`crate::environment_map`] for detailed information. +#[derive(Clone, Component, Reflect)] +pub struct EnvironmentMapLight { + /// The blurry image that represents diffuse radiance surrounding a region. + pub diffuse_map: Handle, + + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub specular_map: Handle, + + /// Scale factor applied to the diffuse and specular light generated by this component. + /// + /// After applying this multiplier, the resulting values should + /// be in units of [cd/m^2](https://en.wikipedia.org/wiki/Candela_per_square_metre). + /// + /// See also . + pub intensity: f32, +} + +/// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles. +/// +/// This is for use in the render app. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct EnvironmentMapIds { + /// The blurry image that represents diffuse radiance surrounding a region. + pub(crate) diffuse: AssetId, + /// The typically-sharper, mipmapped image that represents specular radiance + /// surrounding a region. + pub(crate) specular: AssetId, +} + +/// A bundle that contains everything needed to make an entity a reflection +/// probe. +/// +/// A reflection probe is a type of environment map that specifies the light +/// surrounding a region in space. For more information, see +/// [`crate::environment_map`]. +#[derive(Bundle)] +pub struct ReflectionProbeBundle { + /// Contains a transform that specifies the position of this reflection probe in space. + pub spatial: SpatialBundle, + /// Marks this environment map as a light probe. + pub light_probe: LightProbe, + /// The cubemaps that make up this environment map. + pub environment_map: EnvironmentMapLight, +} + +/// A component, part of the render world, that stores the mapping from +/// environment map ID to texture index in the diffuse and specular binding +/// arrays. +/// +/// Cubemap textures belonging to environment maps are collected into binding +/// arrays, and the index of each texture is presented to the shader for runtime +/// lookup. +/// +/// This component is attached to each view in the render world, because each +/// view may have a different set of cubemaps that it considers and therefore +/// cubemap indices are per-view. +#[derive(Component, Default)] +pub struct RenderViewEnvironmentMaps { + /// The list of environment maps presented to the shader, in order. + binding_index_to_cubemap: Vec, + /// The reverse of `binding_index_to_cubemap`: a map from the environment + /// map IDs to the index in `binding_index_to_cubemap`. + cubemap_to_binding_index: HashMap, +} + +/// All the bind group entries necessary for PBR shaders to access the +/// environment maps exposed to a view. +pub(crate) enum RenderViewBindGroupEntries<'a> { + /// The version used when binding arrays aren't available on the current + /// platform. + Single { + /// The texture view of the view's diffuse cubemap. + diffuse_texture_view: &'a TextureView, + + /// The texture view of the view's specular cubemap. + specular_texture_view: &'a TextureView, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + sampler: &'a Sampler, + }, + + /// The version used when binding arrays aren't available on the current + /// platform. + Multiple { + /// A texture view of each diffuse cubemap, in the same order that they are + /// supplied to the view (i.e. in the same order as + /// `binding_index_to_cubemap` in [`RenderViewEnvironmentMaps`]). + /// + /// This is a vector of `wgpu::TextureView`s. But we don't want to import + /// `wgpu` in this crate, so we refer to it indirectly like this. + diffuse_texture_views: Vec<&'a ::Target>, + + /// As above, but for specular cubemaps. + specular_texture_views: Vec<&'a ::Target>, + + /// The sampler used to sample elements of both `diffuse_texture_views` and + /// `specular_texture_views`. + sampler: &'a Sampler, + }, +} + +impl ExtractInstance for EnvironmentMapIds { + type QueryData = Read; + + type QueryFilter = (); + + fn extract(item: QueryItem<'_, Self::QueryData>) -> Option { + Some(EnvironmentMapIds { + diffuse: item.diffuse_map.id(), + specular: item.specular_map.id(), + }) + } +} + +impl RenderViewEnvironmentMaps { + pub(crate) fn new() -> Self { + Self::default() + } +} + +impl RenderViewEnvironmentMaps { + /// Whether there are no environment maps associated with the view. + pub(crate) fn is_empty(&self) -> bool { + self.binding_index_to_cubemap.is_empty() + } + + /// Adds a cubemap to the list of bindings, if it wasn't there already, and + /// returns its index within that list. + pub(crate) fn get_or_insert_cubemap(&mut self, cubemap_id: &EnvironmentMapIds) -> u32 { + *self + .cubemap_to_binding_index + .entry(*cubemap_id) + .or_insert_with(|| { + let index = self.binding_index_to_cubemap.len() as u32; + self.binding_index_to_cubemap.push(*cubemap_id); + index + }) + } +} + +/// Returns the bind group layout entries for the environment map diffuse and +/// specular binding arrays respectively, in addition to the sampler. +pub(crate) fn get_bind_group_layout_entries( + render_device: &RenderDevice, +) -> [BindGroupLayoutEntryBuilder; 3] { + let mut texture_cube_binding = + binding_types::texture_cube(TextureSampleType::Float { filterable: true }); + if binding_arrays_are_usable(render_device) { + texture_cube_binding = + texture_cube_binding.count(NonZeroU32::new(MAX_VIEW_REFLECTION_PROBES as _).unwrap()); + } + + [ + texture_cube_binding, + texture_cube_binding, + binding_types::sampler(SamplerBindingType::Filtering), + ] +} + +impl<'a> RenderViewBindGroupEntries<'a> { + /// Looks up and returns the bindings for the environment map diffuse and + /// specular binding arrays respectively, as well as the sampler. + pub(crate) fn get( + render_view_environment_maps: Option<&RenderViewEnvironmentMaps>, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, + render_device: &RenderDevice, + ) -> RenderViewBindGroupEntries<'a> { + if binding_arrays_are_usable(render_device) { + let mut diffuse_texture_views = vec![]; + let mut specular_texture_views = vec![]; + let mut sampler = None; + + if let Some(environment_maps) = render_view_environment_maps { + for &cubemap_id in &environment_maps.binding_index_to_cubemap { + add_texture_view( + &mut diffuse_texture_views, + &mut sampler, + cubemap_id.diffuse, + images, + fallback_image, + ); + add_texture_view( + &mut specular_texture_views, + &mut sampler, + cubemap_id.specular, + images, + fallback_image, + ); + } + } + + // Pad out the bindings to the size of the binding array using fallback + // textures. This is necessary on D3D12 and Metal. + diffuse_texture_views.resize( + MAX_VIEW_REFLECTION_PROBES, + &*fallback_image.cube.texture_view, + ); + specular_texture_views.resize( + MAX_VIEW_REFLECTION_PROBES, + &*fallback_image.cube.texture_view, + ); + + return RenderViewBindGroupEntries::Multiple { + diffuse_texture_views, + specular_texture_views, + sampler: sampler.unwrap_or(&fallback_image.cube.sampler), + }; + } + + if let Some(environment_maps) = render_view_environment_maps { + if let Some(cubemap) = environment_maps.binding_index_to_cubemap.first() { + if let (Some(diffuse_image), Some(specular_image)) = + (images.get(cubemap.diffuse), images.get(cubemap.specular)) + { + return RenderViewBindGroupEntries::Single { + diffuse_texture_view: &diffuse_image.texture_view, + specular_texture_view: &specular_image.texture_view, + sampler: &diffuse_image.sampler, + }; + } + } + } + + RenderViewBindGroupEntries::Single { + diffuse_texture_view: &fallback_image.cube.texture_view, + specular_texture_view: &fallback_image.cube.texture_view, + sampler: &fallback_image.cube.sampler, + } + } +} + +/// Adds a diffuse or specular texture view to the `texture_views` list, and +/// populates `sampler` if this is the first such view. +fn add_texture_view<'a>( + texture_views: &mut Vec<&'a ::Target>, + sampler: &mut Option<&'a Sampler>, + image_id: AssetId, + images: &'a RenderAssets, + fallback_image: &'a FallbackImage, +) { + match images.get(image_id) { + None => { + // Use the fallback image if the cubemap isn't loaded yet. + texture_views.push(&*fallback_image.cube.texture_view); + } + Some(image) => { + // If this is the first texture view, populate `sampler`. + if sampler.is_none() { + *sampler = Some(&image.sampler); + } + + texture_views.push(&*image.texture_view); + } + } +} + +/// Many things can go wrong when attempting to use texture binding arrays +/// (a.k.a. bindless textures). This function checks for these pitfalls: +/// +/// 1. If GLSL support is enabled at the feature level, then in debug mode +/// `naga_oil` will attempt to compile all shader modules under GLSL to check +/// validity of names, even if GLSL isn't actually used. This will cause a crash +/// if binding arrays are enabled, because binding arrays are currently +/// unimplemented in the GLSL backend of Naga. Therefore, we disable binding +/// arrays if the `shader_format_glsl` feature is present. +/// +/// 2. If there aren't enough texture bindings available to accommodate all the +/// binding arrays, the driver will panic. So we also bail out if there aren't +/// enough texture bindings available in the fragment shader. +/// +/// 3. If binding arrays aren't supported on the hardware, then we obviously +/// can't use them. +/// +/// If binding arrays aren't usable, we disable reflection probes, as they rely +/// on them. +pub(crate) fn binding_arrays_are_usable(render_device: &RenderDevice) -> bool { + !cfg!(feature = "shader_format_glsl") + && render_device.limits().max_storage_textures_per_shader_stage + >= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_REFLECTION_PROBES) + as u32 + && render_device + .features() + .contains(WgpuFeatures::TEXTURE_BINDING_ARRAY) +} diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl new file mode 100644 index 0000000000000..4bd5f66c4db9e --- /dev/null +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -0,0 +1,176 @@ +#define_import_path bevy_pbr::environment_map + +#import bevy_pbr::mesh_view_bindings as bindings +#import bevy_pbr::mesh_view_bindings::light_probes + +struct EnvironmentMapLight { + diffuse: vec3, + specular: vec3, +}; + +struct EnvironmentMapRadiances { + irradiance: vec3, + radiance: vec3, +} + +// Define two versions of this function, one for the case in which there are +// multiple light probes and one for the case in which only the view light probe +// is present. + +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + perceptual_roughness: f32, + N: vec3, + R: vec3, + world_position: vec3, +) -> EnvironmentMapRadiances { + var radiances: EnvironmentMapRadiances; + + // Search for a reflection probe that contains the fragment. + // + // TODO: Interpolate between multiple reflection probes. + var cubemap_index: i32 = -1; + var intensity: f32 = 1.0; + for (var reflection_probe_index: i32 = 0; + reflection_probe_index < light_probes.reflection_probe_count; + reflection_probe_index += 1) { + let reflection_probe = light_probes.reflection_probes[reflection_probe_index]; + + // Unpack the inverse transform. + let inverse_transpose_transform = mat4x4( + reflection_probe.inverse_transpose_transform[0], + reflection_probe.inverse_transpose_transform[1], + reflection_probe.inverse_transpose_transform[2], + vec4(0.0, 0.0, 0.0, 1.0)); + let inverse_transform = transpose(inverse_transpose_transform); + + // Check to see if the transformed point is inside the unit cube + // centered at the origin. + let probe_space_pos = (inverse_transform * vec4(world_position, 1.0)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5))) { + cubemap_index = reflection_probe.cubemap_index; + intensity = reflection_probe.intensity; + break; + } + } + + // If we didn't find a reflection probe, use the view environment map if applicable. + if (cubemap_index < 0) { + cubemap_index = light_probes.view_cubemap_index; + intensity = light_probes.intensity_for_view; + } + + // If there's no cubemap, bail out. + if (cubemap_index < 0) { + radiances.irradiance = vec3(0.0); + radiances.radiance = vec3(0.0); + return radiances; + } + + // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + let radiance_level = perceptual_roughness * f32(textureNumLevels(bindings::specular_environment_maps[cubemap_index]) - 1u); + +#ifndef LIGHTMAP + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_maps[cubemap_index], + bindings::environment_map_sampler, + vec3(N.xy, -N.z), + 0.0).rgb * intensity; +#endif // LIGHTMAP + + radiances.radiance = textureSampleLevel( + bindings::specular_environment_maps[cubemap_index], + bindings::environment_map_sampler, + vec3(R.xy, -R.z), + radiance_level).rgb * intensity; + + return radiances; +} + +#else // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn compute_radiances( + perceptual_roughness: f32, + N: vec3, + R: vec3, + world_position: vec3, +) -> EnvironmentMapRadiances { + var radiances: EnvironmentMapRadiances; + + if (light_probes.view_cubemap_index < 0) { + radiances.irradiance = vec3(0.0); + radiances.radiance = vec3(0.0); + return radiances; + } + + // Split-sum approximation for image based lighting: https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + // Technically we could use textureNumLevels(specular_environment_map) - 1 here, but we use a uniform + // because textureNumLevels() does not work on WebGL2 + let radiance_level = perceptual_roughness * f32(light_probes.smallest_specular_mip_level_for_view); + + let intensity = light_probes.intensity_for_view; + +#ifndef LIGHTMAP + radiances.irradiance = textureSampleLevel( + bindings::diffuse_environment_map, + bindings::environment_map_sampler, + vec3(N.xy, -N.z), + 0.0).rgb * intensity; +#endif // LIGHTMAP + + radiances.radiance = textureSampleLevel( + bindings::specular_environment_map, + bindings::environment_map_sampler, + vec3(R.xy, -R.z), + radiance_level).rgb * intensity; + + return radiances; +} + +#endif // MULTIPLE_LIGHT_PROBES_IN_ARRAY + +fn environment_map_light( + perceptual_roughness: f32, + roughness: f32, + diffuse_color: vec3, + NdotV: f32, + f_ab: vec2, + N: vec3, + R: vec3, + F0: vec3, + world_position: vec3, +) -> EnvironmentMapLight { + let radiances = compute_radiances(perceptual_roughness, N, R, world_position); + + // No real world material has specular values under 0.02, so we use this range as a + // "pre-baked specular occlusion" that extinguishes the fresnel term, for artistic control. + // See: https://google.github.io/filament/Filament.html#specularocclusion + let specular_occlusion = saturate(dot(F0, vec3(50.0 * 0.33))); + + // Multiscattering approximation: https://www.jcgt.org/published/0008/01/03/paper.pdf + // Useful reference: https://bruop.github.io/ibl + let Fr = max(vec3(1.0 - roughness), F0) - F0; + let kS = F0 + Fr * pow(1.0 - NdotV, 5.0); + let Ess = f_ab.x + f_ab.y; + let FssEss = kS * Ess * specular_occlusion; + let Ems = 1.0 - Ess; + let Favg = F0 + (1.0 - F0) / 21.0; + let Fms = FssEss * Favg / (1.0 - Ems * Favg); + let FmsEms = Fms * Ems; + let Edss = 1.0 - (FssEss + FmsEms); + let kD = diffuse_color * Edss; + + var out: EnvironmentMapLight; + + // If there's a lightmap, ignore the diffuse component of the reflection + // probe, so we don't double-count light. +#ifdef LIGHTMAP + out.diffuse = vec3(0.0); +#else + out.diffuse = (FmsEms + kD) * radiances.irradiance; +#endif + + out.specular = FssEss * radiances.radiance; + return out; +} diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs new file mode 100644 index 0000000000000..c76d661eead56 --- /dev/null +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -0,0 +1,438 @@ +//! Light probes for baked global illumination. + +use bevy_app::{App, Plugin}; +use bevy_asset::load_internal_asset; +use bevy_core_pipeline::core_3d::Camera3d; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + reflect::ReflectComponent, + schedule::IntoSystemConfigs, + system::{Commands, Local, Query, Res, ResMut, Resource}, +}; +use bevy_math::{Affine3A, Mat4, Vec3A, Vec4}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + extract_instances::ExtractInstancesPlugin, + primitives::{Aabb, Frustum}, + render_asset::RenderAssets, + render_resource::{DynamicUniformBuffer, Shader, ShaderType}, + renderer::{RenderDevice, RenderQueue}, + texture::Image, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::{EntityHashMap, FloatOrd}; + +use crate::light_probe::environment_map::{ + binding_arrays_are_usable, EnvironmentMapIds, EnvironmentMapLight, RenderViewEnvironmentMaps, + ENVIRONMENT_MAP_SHADER_HANDLE, +}; + +pub mod environment_map; + +/// The maximum number of reflection probes that each view will consider. +/// +/// Because the fragment shader does a linear search through the list for each +/// fragment, this number needs to be relatively small. +pub const MAX_VIEW_REFLECTION_PROBES: usize = 8; + +/// Adds support for light probes: cuboid bounding regions that apply global +/// illumination to objects within them. +/// +/// This also adds support for view environment maps: diffuse and specular +/// cubemaps applied to all objects that a view renders. +pub struct LightProbePlugin; + +/// A marker component for a light probe, which is a cuboid region that provides +/// global illumination to all fragments inside it. +/// +/// The light probe range is conceptually a unit cube (1×1×1) centered on the +/// origin. The [`bevy_transform::prelude::Transform`] applied to this entity +/// can scale, rotate, or translate that cube so that it contains all fragments +/// that should take this light probe into account. +/// +/// Note that a light probe will have no effect unless the entity contains some +/// kind of illumination. At present, the only supported type of illumination is +/// the [`EnvironmentMapLight`]. +#[derive(Component, Debug, Clone, Copy, Default, Reflect)] +#[reflect(Component, Default)] +pub struct LightProbe; + +/// A GPU type that stores information about a reflection probe. +#[derive(Clone, Copy, ShaderType, Default)] +struct RenderReflectionProbe { + /// The transform from the world space to the model space. This is used to + /// efficiently check for bounding box intersection. + inverse_transpose_transform: [Vec4; 3], + + /// The index of the environment map in the diffuse and specular cubemap + /// binding arrays. + cubemap_index: i32, + + /// Scale factor applied to the diffuse and specular light generated by this + /// reflection probe. + /// + /// See the comment in [`EnvironmentMapLight`] for details. + intensity: f32, +} + +/// A per-view shader uniform that specifies all the light probes that the view +/// takes into account. +#[derive(ShaderType)] +pub struct LightProbesUniform { + /// The list of applicable reflection probes, sorted from nearest to the + /// camera to the farthest away from the camera. + reflection_probes: [RenderReflectionProbe; MAX_VIEW_REFLECTION_PROBES], + + /// The number of reflection probes in the list. + reflection_probe_count: i32, + + /// The index of the diffuse and specular environment maps associated with + /// the view itself. This is used as a fallback if no reflection probe in + /// the list contains the fragment. + view_cubemap_index: i32, + + /// The smallest valid mipmap level for the specular environment cubemap + /// associated with the view. + smallest_specular_mip_level_for_view: u32, + + /// The intensity of the environment cubemap associated with the view. + /// + /// See the comment in [`EnvironmentMapLight`] for details. + intensity_for_view: f32, +} + +/// A map from each camera to the light probe uniform associated with it. +#[derive(Resource, Default, Deref, DerefMut)] +struct RenderLightProbes(EntityHashMap); + +/// A GPU buffer that stores information about all light probes. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct LightProbesBuffer(DynamicUniformBuffer); + +/// A component attached to each camera in the render world that stores the +/// index of the [`LightProbesUniform`] in the [`LightProbesBuffer`]. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewLightProbesUniformOffset(u32); + +/// Information that [`gather_light_probes`] keeps about each light probe. +#[derive(Clone, Copy)] +#[allow(dead_code)] +struct LightProbeInfo { + // The transform from world space to light probe space. + inverse_transform: Mat4, + + // The transform from light probe space to world space. + affine_transform: Affine3A, + + // The diffuse and specular environment maps associated with this light + // probe. + environment_maps: EnvironmentMapIds, + + // Scale factor applied to the diffuse and specular light generated by this + // reflection probe. + // + // See the comment in [`EnvironmentMapLight`] for details. + intensity: f32, +} + +impl LightProbe { + /// Creates a new light probe component. + #[inline] + pub fn new() -> Self { + Self + } +} + +impl Plugin for LightProbePlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + ENVIRONMENT_MAP_SHADER_HANDLE, + "environment_map.wgsl", + Shader::from_wgsl + ); + + app.register_type::() + .register_type::(); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_plugins(ExtractInstancesPlugin::::new()) + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, gather_light_probes) + .add_systems( + Render, + upload_light_probes.in_set(RenderSet::PrepareResources), + ); + } +} + +/// Gathers up all light probes in the scene and assigns them to views, +/// performing frustum culling and distance sorting in the process. +/// +/// This populates the [`RenderLightProbes`] resource. +#[allow(clippy::too_many_arguments)] +fn gather_light_probes( + mut render_light_probes: ResMut, + image_assets: Res>, + render_device: Res, + light_probe_query: Extract>>, + view_query: Extract< + Query< + ( + Entity, + &GlobalTransform, + &Frustum, + Option<&EnvironmentMapLight>, + ), + With, + >, + >, + mut light_probes: Local>, + mut view_light_probes: Local>, + mut commands: Commands, +) { + // Create [`LightProbeInfo`] for every light probe in the scene. + light_probes.clear(); + light_probes.extend( + light_probe_query + .iter() + .filter_map(|query_row| LightProbeInfo::new(query_row, &image_assets)), + ); + + // Build up the light probes uniform and the key table. + render_light_probes.clear(); + for (view_entity, view_transform, view_frustum, view_environment_maps) in view_query.iter() { + // Cull light probes outside the view frustum. + view_light_probes.clear(); + view_light_probes.extend( + light_probes + .iter() + .filter(|light_probe_info| light_probe_info.frustum_cull(view_frustum)) + .cloned(), + ); + + // Sort by distance to camera. + view_light_probes.sort_by_cached_key(|light_probe_info| { + light_probe_info.camera_distance_sort_key(view_transform) + }); + + // Create the light probes uniform. + let (light_probes_uniform, render_view_environment_maps) = LightProbesUniform::build( + view_environment_maps, + &view_light_probes, + &image_assets, + &render_device, + ); + + // Record the uniforms. + render_light_probes.insert(view_entity, light_probes_uniform); + + // Record the per-view environment maps. + let mut commands = commands.get_or_spawn(view_entity); + if render_view_environment_maps.is_empty() { + commands.remove::(); + } else { + commands.insert(render_view_environment_maps); + } + } +} + +/// Uploads the result of [`gather_light_probes`] to the GPU. +fn upload_light_probes( + mut commands: Commands, + light_probes_uniforms: Res, + mut light_probes_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + // Get the uniform buffer writer. + let Some(mut writer) = + light_probes_buffer.get_writer(light_probes_uniforms.len(), &render_device, &render_queue) + else { + return; + }; + + // Send each view's uniforms to the GPU. + for (&view_entity, light_probes_uniform) in light_probes_uniforms.iter() { + commands + .entity(view_entity) + .insert(ViewLightProbesUniformOffset( + writer.write(light_probes_uniform), + )); + } +} + +impl Default for LightProbesUniform { + fn default() -> Self { + Self { + reflection_probes: [RenderReflectionProbe::default(); MAX_VIEW_REFLECTION_PROBES], + reflection_probe_count: 0, + view_cubemap_index: -1, + smallest_specular_mip_level_for_view: 0, + intensity_for_view: 1.0, + } + } +} + +impl LightProbesUniform { + /// Constructs a [`LightProbesUniform`] containing all the environment maps + /// that fragments rendered by a single view need to consider. + /// + /// The `view_environment_maps` parameter describes the environment maps + /// attached to the view. The `light_probes` parameter is expected to be the + /// list of light probes in the scene, sorted by increasing view distance + /// from the camera. + fn build( + view_environment_maps: Option<&EnvironmentMapLight>, + light_probes: &[LightProbeInfo], + image_assets: &RenderAssets, + render_device: &RenderDevice, + ) -> (LightProbesUniform, RenderViewEnvironmentMaps) { + let mut render_view_environment_maps = RenderViewEnvironmentMaps::new(); + + // Find the index of the cubemap associated with the view, and determine + // its smallest mip level. + let mut view_cubemap_index = -1; + let mut smallest_specular_mip_level_for_view = 0; + let mut intensity_for_view = 1.0; + if let Some(EnvironmentMapLight { + diffuse_map: diffuse_map_handle, + specular_map: specular_map_handle, + intensity, + }) = view_environment_maps + { + if let (Some(_), Some(specular_map)) = ( + image_assets.get(diffuse_map_handle), + image_assets.get(specular_map_handle), + ) { + view_cubemap_index = + render_view_environment_maps.get_or_insert_cubemap(&EnvironmentMapIds { + diffuse: diffuse_map_handle.id(), + specular: specular_map_handle.id(), + }) as i32; + smallest_specular_mip_level_for_view = specular_map.mip_level_count - 1; + intensity_for_view = *intensity; + } + }; + + // Initialize the uniform to only contain the view environment map, if + // applicable. + let mut uniform = LightProbesUniform { + reflection_probes: [RenderReflectionProbe::default(); MAX_VIEW_REFLECTION_PROBES], + reflection_probe_count: light_probes.len().min(MAX_VIEW_REFLECTION_PROBES) as i32, + view_cubemap_index, + smallest_specular_mip_level_for_view, + intensity_for_view, + }; + + // Add reflection probes from the scene, if supported by the current + // platform. + uniform.maybe_gather_reflection_probes( + &mut render_view_environment_maps, + light_probes, + render_device, + ); + + (uniform, render_view_environment_maps) + } + + /// Gathers up all reflection probes in the scene and writes them into this + /// uniform and `render_view_environment_maps`. + fn maybe_gather_reflection_probes( + &mut self, + render_view_environment_maps: &mut RenderViewEnvironmentMaps, + light_probes: &[LightProbeInfo], + render_device: &RenderDevice, + ) { + if !binding_arrays_are_usable(render_device) { + return; + } + + for (reflection_probe, light_probe) in self + .reflection_probes + .iter_mut() + .zip(light_probes.iter().take(MAX_VIEW_REFLECTION_PROBES)) + { + // Determine the index of the cubemap in the binding array. + let cubemap_index = render_view_environment_maps + .get_or_insert_cubemap(&light_probe.environment_maps) + as i32; + + // Transpose the inverse transform to compress the structure on the + // GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it + // to recover the original inverse transform. + let inverse_transpose_transform = light_probe.inverse_transform.transpose(); + + // Write in the reflection probe data. + *reflection_probe = RenderReflectionProbe { + inverse_transpose_transform: [ + inverse_transpose_transform.x_axis, + inverse_transpose_transform.y_axis, + inverse_transpose_transform.z_axis, + ], + cubemap_index, + intensity: light_probe.intensity, + }; + } + } +} + +impl LightProbeInfo { + /// Given the set of light probe components, constructs and returns + /// [`LightProbeInfo`]. This is done for every light probe in the scene + /// every frame. + fn new( + (light_probe_transform, environment_map): (&GlobalTransform, &EnvironmentMapLight), + image_assets: &RenderAssets, + ) -> Option { + if image_assets.get(&environment_map.diffuse_map).is_none() + || image_assets.get(&environment_map.specular_map).is_none() + { + return None; + } + + Some(LightProbeInfo { + affine_transform: light_probe_transform.affine(), + inverse_transform: light_probe_transform.compute_matrix().inverse(), + environment_maps: EnvironmentMapIds { + diffuse: environment_map.diffuse_map.id(), + specular: environment_map.specular_map.id(), + }, + intensity: environment_map.intensity, + }) + } + + /// Returns true if this light probe is in the viewing frustum of the camera + /// or false if it isn't. + fn frustum_cull(&self, view_frustum: &Frustum) -> bool { + view_frustum.intersects_obb( + &Aabb { + center: Vec3A::default(), + half_extents: Vec3A::splat(0.5), + }, + &self.affine_transform, + true, + false, + ) + } + + /// Returns the squared distance from this light probe to the camera, + /// suitable for distance sorting. + fn camera_distance_sort_key(&self, view_transform: &GlobalTransform) -> FloatOrd { + FloatOrd( + (self.affine_transform.translation - view_transform.translation_vec3a()) + .length_squared(), + ) + } +} diff --git a/crates/bevy_pbr/src/lightmap/lightmap.wgsl b/crates/bevy_pbr/src/lightmap/lightmap.wgsl new file mode 100644 index 0000000000000..cf3c2275c9a08 --- /dev/null +++ b/crates/bevy_pbr/src/lightmap/lightmap.wgsl @@ -0,0 +1,29 @@ +#define_import_path bevy_pbr::lightmap + +#import bevy_pbr::mesh_bindings::mesh + +@group(1) @binding(4) var lightmaps_texture: texture_2d; +@group(1) @binding(5) var lightmaps_sampler: sampler; + +// Samples the lightmap, if any, and returns indirect illumination from it. +fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { + let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; + let uv_rect = vec4(vec4( + packed_uv_rect.x & 0xffffu, + packed_uv_rect.x >> 16u, + packed_uv_rect.y & 0xffffu, + packed_uv_rect.y >> 16u)) / 65535.0; + + let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv); + + // Mipmapping lightmaps is usually a bad idea due to leaking across UV + // islands, so there's no harm in using mip level 0 and it lets us avoid + // control flow uniformity problems. + // + // TODO(pcwalton): Consider bicubic filtering. + return textureSampleLevel( + lightmaps_texture, + lightmaps_sampler, + lightmap_uv, + 0.0).rgb * exposure; +} diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs new file mode 100644 index 0000000000000..3185507fbb55c --- /dev/null +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -0,0 +1,210 @@ +//! Lightmaps, baked lighting textures that can be applied at runtime to provide +//! diffuse global illumination. +//! +//! Bevy doesn't currently have any way to actually bake lightmaps, but they can +//! be baked in an external tool like [Blender](http://blender.org), for example +//! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`] +//! project support other lightmap baking methods. +//! +//! When a [`Lightmap`] component is added to an entity with a [`Mesh`] and a +//! [`StandardMaterial`](crate::StandardMaterial), Bevy applies the lightmap when rendering. The brightness +//! of the lightmap may be controlled with the `lightmap_exposure` field on +//! `StandardMaterial`. +//! +//! During the rendering extraction phase, we extract all lightmaps into the +//! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup +//! and mesh uniform creation consults this table to determine which lightmap to +//! supply to the shader. Essentially, the lightmap is a special type of texture +//! that is part of the mesh instance rather than part of the material (because +//! multiple meshes can share the same material, whereas sharing lightmaps is +//! nonsensical). +//! +//! Note that meshes can't be instanced if they use different lightmap textures. +//! If you want to instance a lightmapped mesh, combine the lightmap textures +//! into a single atlas, and set the `uv_rect` field on [`Lightmap`] +//! appropriately. +//! +//! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper +//! +//! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, AssetId, Handle}; +use bevy_ecs::{ + component::Component, + entity::Entity, + reflect::ReflectComponent, + schedule::IntoSystemConfigs, + system::{Query, Res, ResMut, Resource}, +}; +use bevy_math::{uvec2, vec4, Rect, UVec2}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + mesh::Mesh, render_asset::RenderAssets, render_resource::Shader, texture::Image, + view::ViewVisibility, Extract, ExtractSchedule, RenderApp, +}; +use bevy_utils::{EntityHashMap, HashSet}; + +use crate::RenderMeshInstances; + +/// The ID of the lightmap shader. +pub const LIGHTMAP_SHADER_HANDLE: Handle = + Handle::weak_from_u128(285484768317531991932943596447919767152); + +/// A plugin that provides an implementation of lightmaps. +pub struct LightmapPlugin; + +/// A component that applies baked indirect diffuse global illumination from a +/// lightmap. +/// +/// When assigned to an entity that contains a [`Mesh`] and a +/// [`StandardMaterial`](crate::StandardMaterial), if the mesh has a second UV +/// layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)), then +/// the lightmap will render using those UVs. +#[derive(Component, Clone, Reflect)] +#[reflect(Component, Default)] +pub struct Lightmap { + /// The lightmap texture. + pub image: Handle, + + /// The rectangle within the lightmap texture that the UVs are relative to. + /// + /// The top left coordinate is the `min` part of the rect, and the bottom + /// right coordinate is the `max` part of the rect. The rect ranges from (0, + /// 0) to (1, 1). + /// + /// This field allows lightmaps for a variety of meshes to be packed into a + /// single atlas. + pub uv_rect: Rect, +} + +/// Lightmap data stored in the render world. +/// +/// There is one of these per visible lightmapped mesh instance. +#[derive(Debug)] +pub(crate) struct RenderLightmap { + /// The ID of the lightmap texture. + pub(crate) image: AssetId, + + /// The rectangle within the lightmap texture that the UVs are relative to. + /// + /// The top left coordinate is the `min` part of the rect, and the bottom + /// right coordinate is the `max` part of the rect. The rect ranges from (0, + /// 0) to (1, 1). + pub(crate) uv_rect: Rect, +} + +/// Stores data for all lightmaps in the render world. +/// +/// This is cleared and repopulated each frame during the `extract_lightmaps` +/// system. +#[derive(Default, Resource)] +pub struct RenderLightmaps { + /// The mapping from every lightmapped entity to its lightmap info. + /// + /// Entities without lightmaps, or for which the mesh or lightmap isn't + /// loaded, won't have entries in this table. + pub(crate) render_lightmaps: EntityHashMap, + + /// All active lightmap images in the scene. + /// + /// Gathering all lightmap images into a set makes mesh bindgroup + /// preparation slightly more efficient, because only one bindgroup needs to + /// be created per lightmap texture. + pub(crate) all_lightmap_images: HashSet>, +} + +impl Plugin for LightmapPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + LIGHTMAP_SHADER_HANDLE, + "lightmap.wgsl", + Shader::from_wgsl + ); + } + + fn finish(&self, app: &mut App) { + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.init_resource::().add_systems( + ExtractSchedule, + extract_lightmaps.after(crate::extract_meshes), + ); + } +} + +/// Extracts all lightmaps from the scene and populates the [`RenderLightmaps`] +/// resource. +fn extract_lightmaps( + mut render_lightmaps: ResMut, + lightmaps: Extract>, + render_mesh_instances: Res, + images: Res>, + meshes: Res>, +) { + // Clear out the old frame's data. + render_lightmaps.render_lightmaps.clear(); + render_lightmaps.all_lightmap_images.clear(); + + // Loop over each entity. + for (entity, view_visibility, lightmap) in lightmaps.iter() { + // Only process visible entities for which the mesh and lightmap are + // both loaded. + if !view_visibility.get() + || images.get(&lightmap.image).is_none() + || !render_mesh_instances + .get(&entity) + .and_then(|mesh_instance| meshes.get(mesh_instance.mesh_asset_id)) + .is_some_and(|mesh| mesh.layout.contains(Mesh::ATTRIBUTE_UV_1.id)) + { + continue; + } + + // Store information about the lightmap in the render world. + render_lightmaps.render_lightmaps.insert( + entity, + RenderLightmap::new(lightmap.image.id(), lightmap.uv_rect), + ); + + // Make a note of the loaded lightmap image so we can efficiently + // process them later during mesh bindgroup creation. + render_lightmaps + .all_lightmap_images + .insert(lightmap.image.id()); + } +} + +impl RenderLightmap { + /// Creates a new lightmap from a texture and a UV rect. + fn new(image: AssetId, uv_rect: Rect) -> Self { + Self { image, uv_rect } + } +} + +/// Packs the lightmap UV rect into 64 bits (4 16-bit unsigned integers). +pub(crate) fn pack_lightmap_uv_rect(maybe_rect: Option) -> UVec2 { + match maybe_rect { + Some(rect) => { + let rect_uvec4 = (vec4(rect.min.x, rect.min.y, rect.max.x, rect.max.y) * 65535.0) + .round() + .as_uvec4(); + uvec2( + rect_uvec4.x | (rect_uvec4.y << 16), + rect_uvec4.z | (rect_uvec4.w << 16), + ) + } + None => UVec2::ZERO, + } +} + +impl Default for Lightmap { + fn default() -> Self { + Self { + image: Default::default(), + uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), + } + } +} diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index d1c521724269c..c002296ca0b4c 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,4 +1,4 @@ -use crate::*; +use crate::{environment_map::RenderViewEnvironmentMaps, *}; use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ @@ -234,7 +234,9 @@ where .after(prepare_materials::), queue_material_meshes:: .in_set(RenderSet::QueueMeshes) - .after(prepare_materials::), + .after(prepare_materials::) + // queue_material_meshes only writes to `material_bind_group_id`, which `queue_shadows` doesn't read + .ambiguous_with(render::queue_shadows::), ), ); } @@ -377,8 +379,8 @@ type DrawMaterial = ( pub struct SetMaterialBindGroup(PhantomData); impl RenderCommand

for SetMaterialBindGroup { type Param = (SRes>, SRes>); - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( @@ -404,7 +406,7 @@ impl RenderCommand

for SetMaterial pub type RenderMaterialInstances = ExtractedInstances>; -const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode) -> MeshPipelineKey { +pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode) -> MeshPipelineKey { match alpha_mode { // Premultiplied and Add share the same pipeline key // They're made distinct in the PBR shader, via `premultiply_alpha()` @@ -416,7 +418,7 @@ const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode) -> MeshPipelineKey { } } -const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { +pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { match tonemapping { Tonemapping::None => MeshPipelineKey::TONEMAP_METHOD_NONE, Tonemapping::Reinhard => MeshPipelineKey::TONEMAP_METHOD_REINHARD, @@ -431,7 +433,7 @@ const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> MeshPipelineKey { } } -const fn screen_space_specular_transmission_pipeline_key( +pub const fn screen_space_specular_transmission_pipeline_key( screen_space_transmissive_blur_quality: ScreenSpaceTransmissionQuality, ) -> MeshPipelineKey { match screen_space_transmissive_blur_quality { @@ -464,15 +466,14 @@ pub fn queue_material_meshes( render_materials: Res>, mut render_mesh_instances: ResMut, render_material_instances: Res>, - images: Res>, + render_lightmaps: Res, mut views: Query<( &ExtractedView, &VisibleEntities, Option<&Tonemapping>, Option<&DebandDither>, - Option<&EnvironmentMapLight>, Option<&ShadowFilteringMethod>, - Option<&ScreenSpaceAmbientOcclusionSettings>, + Has, ( Has, Has, @@ -480,12 +481,13 @@ pub fn queue_material_meshes( Has, ), Option<&Camera3d>, - Option<&TemporalJitter>, + Has, Option<&Projection>, &mut RenderPhase, &mut RenderPhase, &mut RenderPhase, &mut RenderPhase, + Has, )>, ) where M::Data: PartialEq + Eq + Hash + Clone, @@ -495,7 +497,6 @@ pub fn queue_material_meshes( visible_entities, tonemapping, dither, - environment_map, shadow_filter_method, ssao, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), @@ -506,6 +507,7 @@ pub fn queue_material_meshes( mut alpha_mask_phase, mut transmissive_phase, mut transparent_phase, + has_environment_maps, ) in &mut views { let draw_opaque_pbr = opaque_draw_functions.read().id::>(); @@ -532,13 +534,11 @@ pub fn queue_material_meshes( view_key |= MeshPipelineKey::DEFERRED_PREPASS; } - if temporal_jitter.is_some() { + if temporal_jitter { view_key |= MeshPipelineKey::TEMPORAL_JITTER; } - let environment_map_loaded = environment_map.is_some_and(|map| map.is_loaded(&images)); - - if environment_map_loaded { + if has_environment_maps { view_key |= MeshPipelineKey::ENVIRONMENT_MAP; } @@ -570,7 +570,7 @@ pub fn queue_material_meshes( view_key |= MeshPipelineKey::DEBAND_DITHER; } } - if ssao.is_some() { + if ssao { view_key |= MeshPipelineKey::SCREEN_SPACE_AMBIENT_OCCLUSION; } if let Some(camera_3d) = camera_3d { @@ -606,8 +606,20 @@ pub fn queue_material_meshes( if mesh.morph_targets.is_some() { mesh_key |= MeshPipelineKey::MORPH_TARGETS; } + + if material.properties.reads_view_transmission_texture { + mesh_key |= MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE; + } + mesh_key |= alpha_mode_pipeline_key(material.properties.alpha_mode); + if render_lightmaps + .render_lightmaps + .contains_key(visible_entity) + { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &material_pipeline, @@ -809,6 +821,7 @@ pub fn extract_materials( let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); for event in events.read() { + #[allow(clippy::match_same_arms)] match event { AssetEvent::Added { id } | AssetEvent::Modified { id } => { changed_assets.insert(*id); @@ -817,6 +830,7 @@ pub fn extract_materials( changed_assets.remove(id); removed.push(*id); } + AssetEvent::Unused { .. } => {} AssetEvent::LoadedWithDependencies { .. } => { // TODO: handle this } diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 77257bc8daadb..ef106c010e793 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -462,6 +462,9 @@ pub struct StandardMaterial { /// Default is `16.0`. pub max_parallax_layer_count: f32, + /// The exposure (brightness) level of the lightmap, if present. + pub lightmap_exposure: f32, + /// Render method used for opaque materials. (Where `alpha_mode` is [`AlphaMode::Opaque`] or [`AlphaMode::Mask`]) pub opaque_render_method: OpaqueRendererMethod, @@ -513,6 +516,7 @@ impl Default for StandardMaterial { depth_map: None, parallax_depth_scale: 0.1, max_parallax_layer_count: 16.0, + lightmap_exposure: 1.0, parallax_mapping_method: ParallaxMappingMethod::Occlusion, opaque_render_method: OpaqueRendererMethod::Auto, deferred_lighting_pass_id: DEFAULT_PBR_DEFERRED_LIGHTING_PASS_ID, @@ -549,27 +553,27 @@ bitflags::bitflags! { /// This is accessible in the shader in the [`StandardMaterialUniform`] #[repr(transparent)] pub struct StandardMaterialFlags: u32 { - const BASE_COLOR_TEXTURE = (1 << 0); - const EMISSIVE_TEXTURE = (1 << 1); - const METALLIC_ROUGHNESS_TEXTURE = (1 << 2); - const OCCLUSION_TEXTURE = (1 << 3); - const DOUBLE_SIDED = (1 << 4); - const UNLIT = (1 << 5); - const TWO_COMPONENT_NORMAL_MAP = (1 << 6); - const FLIP_NORMAL_MAP_Y = (1 << 7); - const FOG_ENABLED = (1 << 8); - const DEPTH_MAP = (1 << 9); // Used for parallax mapping - const SPECULAR_TRANSMISSION_TEXTURE = (1 << 10); - const THICKNESS_TEXTURE = (1 << 11); - const DIFFUSE_TRANSMISSION_TEXTURE = (1 << 12); - const ATTENUATION_ENABLED = (1 << 13); - const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode` - const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into - const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7. - const ALPHA_MODE_BLEND = (2 << Self::ALPHA_MODE_SHIFT_BITS); // - const ALPHA_MODE_PREMULTIPLIED = (3 << Self::ALPHA_MODE_SHIFT_BITS); // - const ALPHA_MODE_ADD = (4 << Self::ALPHA_MODE_SHIFT_BITS); // Right now only values 0–5 are used, which still gives - const ALPHA_MODE_MULTIPLY = (5 << Self::ALPHA_MODE_SHIFT_BITS); // ← us "room" for two more modes without adding more bits + const BASE_COLOR_TEXTURE = 1 << 0; + const EMISSIVE_TEXTURE = 1 << 1; + const METALLIC_ROUGHNESS_TEXTURE = 1 << 2; + const OCCLUSION_TEXTURE = 1 << 3; + const DOUBLE_SIDED = 1 << 4; + const UNLIT = 1 << 5; + const TWO_COMPONENT_NORMAL_MAP = 1 << 6; + const FLIP_NORMAL_MAP_Y = 1 << 7; + const FOG_ENABLED = 1 << 8; + const DEPTH_MAP = 1 << 9; // Used for parallax mapping + const SPECULAR_TRANSMISSION_TEXTURE = 1 << 10; + const THICKNESS_TEXTURE = 1 << 11; + const DIFFUSE_TRANSMISSION_TEXTURE = 1 << 12; + const ATTENUATION_ENABLED = 1 << 13; + const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; // ← Bitmask reserving bits for the `AlphaMode` + const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; // ← Values are just sequential values bitshifted into + const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; // the bitmask, and can range from 0 to 7. + const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS; // + const ALPHA_MODE_PREMULTIPLIED = 3 << Self::ALPHA_MODE_SHIFT_BITS; // + const ALPHA_MODE_ADD = 4 << Self::ALPHA_MODE_SHIFT_BITS; // Right now only values 0–5 are used, which still gives + const ALPHA_MODE_MULTIPLY = 5 << Self::ALPHA_MODE_SHIFT_BITS; // ← us "room" for two more modes without adding more bits const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -621,6 +625,8 @@ pub struct StandardMaterialUniform { /// If your `parallax_depth_scale` is >0.1 and you are seeing jaggy edges, /// increase this value. However, this incurs a performance cost. pub max_parallax_layer_count: f32, + /// The exposure (brightness) level of the lightmap, if present. + pub lightmap_exposure: f32, /// Using [`ParallaxMappingMethod::Relief`], how many additional /// steps to use at most to find the depth value. pub max_relief_mapping_search_steps: u32, @@ -720,6 +726,7 @@ impl AsBindGroupShaderType for StandardMaterial { alpha_cutoff, parallax_depth_scale: self.parallax_depth_scale, max_parallax_layer_count: self.max_parallax_layer_count, + lightmap_exposure: self.lightmap_exposure, max_relief_mapping_search_steps: self.parallax_mapping_method.max_steps(), deferred_lighting_pass_id: self.deferred_lighting_pass_id as u32, } @@ -750,40 +757,6 @@ impl From<&StandardMaterial> for StandardMaterialKey { } impl Material for StandardMaterial { - fn specialize( - _pipeline: &MaterialPipeline, - descriptor: &mut RenderPipelineDescriptor, - _layout: &MeshVertexBufferLayout, - key: MaterialPipelineKey, - ) -> Result<(), SpecializedMeshPipelineError> { - if let Some(fragment) = descriptor.fragment.as_mut() { - let shader_defs = &mut fragment.shader_defs; - - if key.bind_group_data.normal_map { - shader_defs.push("STANDARDMATERIAL_NORMAL_MAP".into()); - } - if key.bind_group_data.relief_mapping { - shader_defs.push("RELIEF_MAPPING".into()); - } - } - descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; - if let Some(label) = &mut descriptor.label { - *label = format!("pbr_{}", *label).into(); - } - if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { - depth_stencil.bias.constant = key.bind_group_data.depth_bias; - } - Ok(()) - } - - fn prepass_fragment_shader() -> ShaderRef { - PBR_PREPASS_SHADER_HANDLE.into() - } - - fn deferred_fragment_shader() -> ShaderRef { - PBR_SHADER_HANDLE.into() - } - fn fragment_shader() -> ShaderRef { PBR_SHADER_HANDLE.into() } @@ -793,16 +766,6 @@ impl Material for StandardMaterial { self.alpha_mode } - #[inline] - fn depth_bias(&self) -> f32 { - self.depth_bias - } - - #[inline] - fn reads_view_transmission_texture(&self) -> bool { - self.specular_transmission > 0.0 - } - #[inline] fn opaque_render_method(&self) -> OpaqueRendererMethod { match self.opaque_render_method { @@ -819,4 +782,48 @@ impl Material for StandardMaterial { other => other, } } + + #[inline] + fn depth_bias(&self) -> f32 { + self.depth_bias + } + + #[inline] + fn reads_view_transmission_texture(&self) -> bool { + self.specular_transmission > 0.0 + } + + fn prepass_fragment_shader() -> ShaderRef { + PBR_PREPASS_SHADER_HANDLE.into() + } + + fn deferred_fragment_shader() -> ShaderRef { + PBR_SHADER_HANDLE.into() + } + + fn specialize( + _pipeline: &MaterialPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayout, + key: MaterialPipelineKey, + ) -> Result<(), SpecializedMeshPipelineError> { + if let Some(fragment) = descriptor.fragment.as_mut() { + let shader_defs = &mut fragment.shader_defs; + + if key.bind_group_data.normal_map { + shader_defs.push("STANDARDMATERIAL_NORMAL_MAP".into()); + } + if key.bind_group_data.relief_mapping { + shader_defs.push("RELIEF_MAPPING".into()); + } + } + descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; + if let Some(label) = &mut descriptor.label { + *label = format!("pbr_{}", *label).into(); + } + if let Some(depth_stencil) = descriptor.depth_stencil.as_mut() { + depth_stencil.bias.constant = key.bind_group_data.depth_bias; + } + Ok(()) + } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 827f458982f7e..3ac2bee99e2ca 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -98,6 +98,7 @@ where ) .init_resource::() .init_resource::>>() + .allow_ambiguous_resource::>>() .init_resource::(); } @@ -168,7 +169,9 @@ where Render, queue_prepass_material_meshes:: .in_set(RenderSet::QueueMeshes) - .after(prepare_materials::), + .after(prepare_materials::) + // queue_material_meshes only writes to `material_bind_group_id`, which `queue_prepass_material_meshes` doesn't read + .ambiguous_with(queue_material_meshes::), ); } } @@ -366,6 +369,11 @@ where vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1)); } + if layout.contains(Mesh::ATTRIBUTE_UV_1) { + shader_defs.push("VERTEX_UVS_B".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(2)); + } + if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { shader_defs.push("NORMAL_PREPASS".into()); } @@ -374,11 +382,11 @@ where .mesh_key .intersects(MeshPipelineKey::NORMAL_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) { - vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(2)); + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(3)); shader_defs.push("NORMAL_PREPASS_OR_DEFERRED_PREPASS".into()); if layout.contains(Mesh::ATTRIBUTE_TANGENT) { shader_defs.push("VERTEX_TANGENTS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); } } @@ -395,7 +403,7 @@ where if layout.contains(Mesh::ATTRIBUTE_COLOR) { shader_defs.push("VERTEX_COLORS".into()); - vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(6)); + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(7)); } if key @@ -416,7 +424,7 @@ where let bind_group = setup_morph_and_skinning_defs( &self.mesh_layouts, layout, - 4, + 5, &key.mesh_key, &mut shader_defs, &mut vertex_attributes, @@ -681,6 +689,7 @@ pub fn queue_prepass_material_meshes( render_mesh_instances: Res, render_materials: Res>, render_material_instances: Res>, + render_lightmaps: Res, mut views: Query< ( &ExtractedView, @@ -793,6 +802,18 @@ pub fn queue_prepass_material_meshes( mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; } + // Even though we don't use the lightmap in the prepass, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. + if render_lightmaps + .render_lightmaps + .contains_key(visible_entity) + { + mesh_key |= MeshPipelineKey::LIGHTMAPPED; + } + let pipeline_id = pipelines.specialize( &pipeline_cache, &prepass_pipeline, @@ -874,11 +895,11 @@ pub fn queue_prepass_material_meshes( pub struct SetPrepassViewBindGroup; impl RenderCommand

for SetPrepassViewBindGroup { type Param = SRes; - type ViewData = ( + type ViewQuery = ( Read, Option>, ); - type ItemData = (); + type ItemQuery = (); #[inline] fn render<'w>( diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index 08b5155de3f81..98795db5ac630 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -62,6 +62,10 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.uv = vertex.uv; #endif // VERTEX_UVS +#ifdef VERTEX_UVS_B + out.uv_b = vertex.uv_b; +#endif // VERTEX_UVS_B + #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED out.world_normal = skinning::skin_normals(model, vertex.normal); diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.rs b/crates/bevy_pbr/src/prepass/prepass_bindings.rs index 77398ce6e587b..3c66625ed8254 100644 --- a/crates/bevy_pbr/src/prepass/prepass_bindings.rs +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.rs @@ -64,19 +64,12 @@ pub fn get_bindings(prepass_textures: Option<&ViewPrepassTextures>) -> [Option, #endif +#ifdef VERTEX_UVS_B + @location(2) uv_b: vec2, +#endif + #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS - @location(2) normal: vec3, + @location(3) normal: vec3, #ifdef VERTEX_TANGENTS - @location(3) tangent: vec4, + @location(4) tangent: vec4, #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED - @location(4) joint_indices: vec4, - @location(5) joint_weights: vec4, + @location(5) joint_indices: vec4, + @location(6) joint_weights: vec4, #endif #ifdef VERTEX_COLORS - @location(6) color: vec4, + @location(7) color: vec4, #endif #ifdef MORPH_TARGETS @@ -40,27 +44,31 @@ struct VertexOutput { @location(0) uv: vec2, #endif +#ifdef VERTEX_UVS_B + @location(1) uv_b: vec2, +#endif + #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS - @location(1) world_normal: vec3, + @location(2) world_normal: vec3, #ifdef VERTEX_TANGENTS - @location(2) world_tangent: vec4, + @location(3) world_tangent: vec4, #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS - @location(3) world_position: vec4, + @location(4) world_position: vec4, #ifdef MOTION_VECTOR_PREPASS - @location(4) previous_world_position: vec4, + @location(5) previous_world_position: vec4, #endif #ifdef DEPTH_CLAMP_ORTHO - @location(5) clip_position_unclamped: vec4, + @location(6) clip_position_unclamped: vec4, #endif // DEPTH_CLAMP_ORTHO #ifdef VERTEX_OUTPUT_INSTANCE_INDEX - @location(6) instance_index: u32, + @location(7) instance_index: u32, #endif #ifdef VERTEX_COLORS - @location(7) color: vec4, + @location(8) color: vec4, #endif } diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 97567486de444..2c861784c1f15 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -11,7 +11,9 @@ struct Vertex { #ifdef VERTEX_UVS @location(2) uv: vec2, #endif -// (Alternate UVs are at location 3, but they're currently unused here.) +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif #ifdef VERTEX_TANGENTS @location(4) tangent: vec4, #endif @@ -36,14 +38,17 @@ struct VertexOutput { #ifdef VERTEX_UVS @location(2) uv: vec2, #endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif #ifdef VERTEX_TANGENTS - @location(3) world_tangent: vec4, + @location(4) world_tangent: vec4, #endif #ifdef VERTEX_COLORS - @location(4) color: vec4, + @location(5) color: vec4, #endif #ifdef VERTEX_OUTPUT_INSTANCE_INDEX - @location(5) @interpolate(flat) instance_index: u32, + @location(6) @interpolate(flat) instance_index: u32, #endif } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 60d6541b2952e..3be78ffd452e3 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -18,7 +18,7 @@ use bevy_transform::{components::GlobalTransform, prelude::Transform}; use bevy_utils::{ nonmax::NonMaxU32, tracing::{error, warn}, - HashMap, + EntityHashMap, }; use std::{hash::Hash, num::NonZeroU64, ops::Range}; @@ -47,7 +47,7 @@ pub struct ExtractedDirectionalLight { shadow_depth_bias: f32, shadow_normal_bias: f32, cascade_shadow_config: CascadeShadowConfig, - cascades: HashMap>, + cascades: EntityHashMap>, render_layers: RenderLayers, } @@ -145,8 +145,8 @@ impl GpuPointLights { bitflags::bitflags! { #[repr(transparent)] struct PointLightFlags: u32 { - const SHADOWS_ENABLED = (1 << 0); - const SPOT_LIGHT_Y_NEGATIVE = (1 << 1); + const SHADOWS_ENABLED = 1 << 0; + const SPOT_LIGHT_Y_NEGATIVE = 1 << 1; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -177,7 +177,7 @@ pub struct GpuDirectionalLight { bitflags::bitflags! { #[repr(transparent)] struct DirectionalLightFlags: u32 { - const SHADOWS_ENABLED = (1 << 0); + const SHADOWS_ENABLED = 1 << 0; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -196,7 +196,6 @@ pub struct GpuLights { n_directional_lights: u32, // offset from spot light's light index to spot light's shadow map index spot_light_shadowmap_offset: i32, - environment_map_smallest_specular_mip_level: u32, } // NOTE: this must be kept in sync with the same constants in pbr.frag @@ -521,7 +520,7 @@ fn face_index_to_name(face_index: usize) -> &'static str { #[derive(Component)] pub struct ShadowView { - pub depth_texture_view: TextureView, + pub depth_attachment: DepthAttachment, pub pass_name: String, } @@ -550,7 +549,7 @@ pub const CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT: u32 = 3; #[derive(Resource)] pub struct GlobalLightMeta { pub gpu_point_lights: GpuPointLights, - pub entity_to_index: HashMap, + pub entity_to_index: EntityHashMap, } impl FromWorld for GlobalLightMeta { @@ -567,7 +566,7 @@ impl GlobalLightMeta { pub fn new(buffer_binding_type: BufferBindingType) -> Self { Self { gpu_point_lights: GpuPointLights::new(buffer_binding_type), - entity_to_index: HashMap::default(), + entity_to_index: EntityHashMap::default(), } } } @@ -644,18 +643,12 @@ pub(crate) fn spot_light_projection_matrix(angle: f32) -> Mat4 { pub fn prepare_lights( mut commands: Commands, mut texture_cache: ResMut, - images: Res>, render_device: Res, render_queue: Res, mut global_light_meta: ResMut, mut light_meta: ResMut, views: Query< - ( - Entity, - &ExtractedView, - &ExtractedClusterConfig, - Option<&EnvironmentMapLight>, - ), + (Entity, &ExtractedView, &ExtractedClusterConfig), With>, >, ambient_light: Res, @@ -857,18 +850,6 @@ pub fn prepare_lights( flags |= DirectionalLightFlags::SHADOWS_ENABLED; } - // convert from illuminance (lux) to candelas - // - // exposure is hard coded at the moment but should be replaced - // by values coming from the camera - // see: https://google.github.io/filament/Filament.html#imagingpipeline/physicallybasedcamera/exposuresettings - const APERTURE: f32 = 4.0; - const SHUTTER_SPEED: f32 = 1.0 / 250.0; - const SENSITIVITY: f32 = 100.0; - let ev100 = f32::log2(APERTURE * APERTURE / SHUTTER_SPEED) - f32::log2(SENSITIVITY / 100.0); - let exposure = 1.0 / (f32::powf(2.0, ev100) * 1.2); - let intensity = light.illuminance * exposure; - let num_cascades = light .cascade_shadow_config .bounds @@ -877,9 +858,9 @@ pub fn prepare_lights( gpu_directional_lights[index] = GpuDirectionalLight { // Filled in later. cascades: [GpuDirectionalCascade::default(); MAX_CASCADES_PER_LIGHT], - // premultiply color by intensity + // 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.as_linear_rgba_f32()) * intensity, + color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * light.illuminance, // direction is negated to be ready for N.L dir_to_light: light.transform.back(), flags: flags.bits(), @@ -901,7 +882,7 @@ pub fn prepare_lights( .write_buffer(&render_device, &render_queue); // set up light data for each view - for (entity, extracted_view, clusters, environment_map) in &views { + for (entity, extracted_view, clusters) in &views { let point_light_depth_texture = texture_cache.get( &render_device, TextureDescriptor { @@ -968,10 +949,6 @@ pub fn prepare_lights( // index to shadow map index, we need to subtract point light count and add directional shadowmap count. spot_light_shadowmap_offset: num_directional_cascades_enabled as i32 - point_light_count as i32, - environment_map_smallest_specular_mip_level: environment_map - .and_then(|env_map| images.get(&env_map.specular_map)) - .map(|specular_map| specular_map.mip_level_count - 1) - .unwrap_or(0), }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query @@ -1008,7 +985,7 @@ pub fn prepare_lights( let view_light_entity = commands .spawn(( ShadowView { - depth_texture_view, + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), pass_name: format!( "shadow pass point light {} {}", light_index, @@ -1070,7 +1047,7 @@ pub fn prepare_lights( let view_light_entity = commands .spawn(( ShadowView { - depth_texture_view, + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), pass_name: format!("shadow pass spot light {light_index}"), }, ExtractedView { @@ -1135,7 +1112,7 @@ pub fn prepare_lights( let view_light_entity = commands .spawn(( ShadowView { - depth_texture_view, + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), pass_name: format!( "shadow pass directional light {light_index} cascade {cascade_index}"), }, @@ -1767,14 +1744,9 @@ impl Node for ShadowPassNode { render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some(&view_light.pass_name), color_attachments: &[], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &view_light.depth_texture_view, - depth_ops: Some(Operations { - load: LoadOp::Clear(0.0), - store: StoreOp::Store, - }), - stencil_ops: None, - }), + depth_stencil_attachment: Some( + view_light.depth_attachment.get_attachment(StoreOp::Store), + ), timestamp_writes: None, occlusion_query_set: None, }); diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index b9bb6195ad9c9..c905332b1fbb6 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,3 +1,8 @@ +use crate::{ + MaterialBindGroupId, NotShadowCaster, NotShadowReceiver, PreviousGlobalTransform, Shadow, + ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, + CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS, +}; use bevy_app::{Plugin, PostUpdate}; use bevy_asset::{load_internal_asset, AssetId, Handle}; use bevy_core_pipeline::{ @@ -7,10 +12,10 @@ use bevy_core_pipeline::{ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::*, - query::{QueryItem, ROQueryItem}, + query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_math::{Affine3, Vec4}; +use bevy_math::{Affine3, Rect, UVec2, Vec4}; use bevy_render::{ batching::{ batch_and_prepare_render_phase, write_batched_instance_buffer, GetBatchData, @@ -21,12 +26,14 @@ use bevy_render::{ render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::*, renderer::{RenderDevice, RenderQueue}, - texture::*, + texture::{ + BevyDefault, DefaultImageSampler, GpuImage, Image, ImageSampler, TextureFormatPixelInfo, + }, view::{ViewTarget, ViewUniformOffset, ViewVisibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::{tracing::error, EntityHashMap, HashMap, Hashed}; +use bevy_utils::{tracing::error, EntityHashMap, Entry, HashMap, Hashed}; use std::cell::Cell; use thread_local::ThreadLocal; @@ -48,6 +55,8 @@ use crate::render::{ }; use crate::*; +use self::environment_map::binding_arrays_are_usable; + use super::skin::SkinIndices; #[derive(Default)] @@ -123,6 +132,7 @@ impl Plugin for MeshRenderPlugin { .init_resource::() .init_resource::() .init_resource::() + .allow_ambiguous_resource::>() .add_systems( ExtractSchedule, (extract_meshes, extract_skins, extract_morphs), @@ -195,6 +205,16 @@ pub struct MeshUniform { // Affine 4x3 matrices transposed to 3x4 pub transform: [Vec4; 3], pub previous_transform: [Vec4; 3], + // Four 16-bit unsigned normalized UV values packed into a `UVec2`: + // + // <--- MSB LSB ---> + // +---- min v ----+ +---- min u ----+ + // lightmap_uv_rect.x: vvvvvvvv vvvvvvvv uuuuuuuu uuuuuuuu, + // +---- max v ----+ +---- max u ----+ + // lightmap_uv_rect.y: VVVVVVVV VVVVVVVV UUUUUUUU UUUUUUUU, + // + // (MSB: most significant bit; LSB: least significant bit.) + pub lightmap_uv_rect: UVec2, // 3x3 matrix packed in mat2x4 and f32 as: // [0].xyz, [1].x, // [1].yz, [2].xy @@ -204,13 +224,14 @@ pub struct MeshUniform { pub flags: u32, } -impl From<&MeshTransforms> for MeshUniform { - fn from(mesh_transforms: &MeshTransforms) -> Self { +impl MeshUniform { + fn new(mesh_transforms: &MeshTransforms, maybe_lightmap_uv_rect: Option) -> Self { let (inverse_transpose_model_a, inverse_transpose_model_b) = mesh_transforms.transform.inverse_transpose_3x3(); Self { transform: mesh_transforms.transform.to_transpose(), previous_transform: mesh_transforms.previous_transform.to_transpose(), + lightmap_uv_rect: lightmap::pack_lightmap_uv_rect(maybe_lightmap_uv_rect), inverse_transpose_model_a, inverse_transpose_model_b, flags: mesh_transforms.flags, @@ -222,11 +243,11 @@ impl From<&MeshTransforms> for MeshUniform { bitflags::bitflags! { #[repr(transparent)] pub struct MeshFlags: u32 { - const SHADOW_RECEIVER = (1 << 0); - const TRANSMITTED_SHADOW_RECEIVER = (1 << 1); + const SHADOW_RECEIVER = 1 << 0; + const TRANSMITTED_SHADOW_RECEIVER = 1 << 1; // Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive, // then the flag should be set, else it should not be set. - const SIGN_DETERMINANT_MODEL_3X3 = (1 << 31); + const SIGN_DETERMINANT_MODEL_3X3 = 1 << 31; const NONE = 0; const UNINITIALIZED = 0xFFFF; } @@ -272,9 +293,9 @@ pub fn extract_meshes( transform, previous_transform, handle, - not_receiver, + not_shadow_receiver, transmitted_receiver, - not_caster, + not_shadow_caster, no_automatic_batching, )| { if !view_visibility.get() { @@ -282,7 +303,7 @@ pub fn extract_meshes( } let transform = transform.affine(); let previous_transform = previous_transform.map(|t| t.0).unwrap_or(transform); - let mut flags = if not_receiver { + let mut flags = if not_shadow_receiver { MeshFlags::empty() } else { MeshFlags::SHADOW_RECEIVER @@ -305,7 +326,7 @@ pub fn extract_meshes( RenderMeshInstance { mesh_asset_id: handle.id(), transforms, - shadow_caster: !not_caster, + shadow_caster: !not_shadow_caster, material_bind_group_id: MaterialBindGroupId::default(), automatic_batching: !no_automatic_batching, }, @@ -346,6 +367,12 @@ pub struct MeshPipeline { /// ``` pub per_object_buffer_batch_size: Option, + /// Whether binding arrays (a.k.a. bindless textures) are usable on the + /// current render device. + /// + /// This affects whether reflection probes can be used. + pub binding_arrays_are_usable: bool, + #[cfg(debug_assertions)] pub did_warn_about_too_many_textures: Arc, } @@ -404,6 +431,7 @@ impl FromWorld for MeshPipeline { dummy_white_gpu_image, mesh_layouts: MeshLayouts::new(&render_device), per_object_buffer_batch_size: GpuArrayBuffer::::batch_size(&render_device), + binding_arrays_are_usable: binding_arrays_are_usable(&render_device), #[cfg(debug_assertions)] did_warn_about_too_many_textures: Arc::new(AtomicBool::new(false)), } @@ -447,26 +475,31 @@ impl MeshPipeline { } impl GetBatchData for MeshPipeline { - type Param = SRes; - type Data = Entity; - type Filter = With; - type CompareData = (MaterialBindGroupId, AssetId); + type Param = (SRes, SRes); + // The material bind group ID, the mesh ID, and the lightmap ID, + // respectively. + type CompareData = (MaterialBindGroupId, AssetId, Option>); + type BufferData = MeshUniform; fn get_batch_data( - mesh_instances: &SystemParamItem, - entity: &QueryItem, - ) -> (Self::BufferData, Option) { - let mesh_instance = mesh_instances - .get(entity) - .expect("Failed to find render mesh instance"); - ( - (&mesh_instance.transforms).into(), + (mesh_instances, lightmaps): &SystemParamItem, + entity: Entity, + ) -> Option<(Self::BufferData, Option)> { + let mesh_instance = mesh_instances.get(&entity)?; + let maybe_lightmap = lightmaps.render_lightmaps.get(&entity); + + Some(( + MeshUniform::new( + &mesh_instance.transforms, + maybe_lightmap.map(|lightmap| lightmap.uv_rect), + ), mesh_instance.automatic_batching.then_some(( mesh_instance.material_bind_group_id, mesh_instance.mesh_asset_id, + maybe_lightmap.map(|lightmap| lightmap.image), )), - ) + )) } } @@ -477,25 +510,27 @@ bitflags::bitflags! { /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. pub struct MeshPipelineKey: u32 { const NONE = 0; - const HDR = (1 << 0); - const TONEMAP_IN_SHADER = (1 << 1); - const DEBAND_DITHER = (1 << 2); - const DEPTH_PREPASS = (1 << 3); - const NORMAL_PREPASS = (1 << 4); - const DEFERRED_PREPASS = (1 << 5); - const MOTION_VECTOR_PREPASS = (1 << 6); - const MAY_DISCARD = (1 << 7); // Guards shader codepaths that may discard, allowing early depth tests in most cases + const HDR = 1 << 0; + const TONEMAP_IN_SHADER = 1 << 1; + const DEBAND_DITHER = 1 << 2; + const DEPTH_PREPASS = 1 << 3; + const NORMAL_PREPASS = 1 << 4; + const DEFERRED_PREPASS = 1 << 5; + const MOTION_VECTOR_PREPASS = 1 << 6; + const MAY_DISCARD = 1 << 7; // Guards shader codepaths that may discard, allowing early depth tests in most cases // See: https://www.khronos.org/opengl/wiki/Early_Fragment_Test - const ENVIRONMENT_MAP = (1 << 8); - const SCREEN_SPACE_AMBIENT_OCCLUSION = (1 << 9); - const DEPTH_CLAMP_ORTHO = (1 << 10); - const TEMPORAL_JITTER = (1 << 11); - const MORPH_TARGETS = (1 << 12); + const ENVIRONMENT_MAP = 1 << 8; + const SCREEN_SPACE_AMBIENT_OCCLUSION = 1 << 9; + const DEPTH_CLAMP_ORTHO = 1 << 10; + const TEMPORAL_JITTER = 1 << 11; + const MORPH_TARGETS = 1 << 12; + const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 13; + const LIGHTMAPPED = 1 << 14; const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state - const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3 - const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); // - const BLEND_MULTIPLY = (2 << Self::BLEND_SHIFT_BITS); // ← We still have room for one more value without adding more bits - const BLEND_ALPHA = (3 << Self::BLEND_SHIFT_BITS); + const BLEND_OPAQUE = 0 << Self::BLEND_SHIFT_BITS; // ← Values are just sequential within the mask, and can range from 0 to 3 + const BLEND_PREMULTIPLIED_ALPHA = 1 << Self::BLEND_SHIFT_BITS; // + const BLEND_MULTIPLY = 2 << Self::BLEND_SHIFT_BITS; // ← We still have room for one more value without adding more bits + const BLEND_ALPHA = 3 << Self::BLEND_SHIFT_BITS; const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; @@ -608,21 +643,23 @@ pub fn setup_morph_and_skinning_defs( vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(offset + 1)); }; let is_morphed = key.intersects(MeshPipelineKey::MORPH_TARGETS); - match (is_skinned(layout), is_morphed) { - (true, false) => { + let is_lightmapped = key.intersects(MeshPipelineKey::LIGHTMAPPED); + match (is_skinned(layout), is_morphed, is_lightmapped) { + (true, false, _) => { add_skin_data(); mesh_layouts.skinned.clone() } - (true, true) => { + (true, true, _) => { add_skin_data(); shader_defs.push("MORPH_TARGETS".into()); mesh_layouts.morphed_skinned.clone() } - (false, true) => { + (false, true, _) => { shader_defs.push("MORPH_TARGETS".into()); mesh_layouts.morphed.clone() } - (false, false) => mesh_layouts.model_only.clone(), + (false, false, true) => mesh_layouts.lightmapped.clone(), + (false, false, false) => mesh_layouts.model_only.clone(), } } @@ -658,7 +695,7 @@ impl SpecializedMeshPipeline for MeshPipeline { } if layout.contains(Mesh::ATTRIBUTE_UV_1) { - shader_defs.push("VERTEX_UVS_1".into()); + shader_defs.push("VERTEX_UVS_B".into()); vertex_attributes.push(Mesh::ATTRIBUTE_UV_1.at_shader_location(3)); } @@ -737,7 +774,7 @@ impl SpecializedMeshPipeline for MeshPipeline { // the current fragment value in the output and the depth is written to the // depth buffer depth_write_enabled = true; - is_opaque = true; + is_opaque = !key.contains(MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE); } if key.contains(MeshPipelineKey::NORMAL_PREPASS) { @@ -809,6 +846,10 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("ENVIRONMENT_MAP".into()); } + if key.contains(MeshPipelineKey::LIGHTMAPPED) { + shader_defs.push("LIGHTMAP".into()); + } + if key.contains(MeshPipelineKey::TEMPORAL_JITTER) { shader_defs.push("TEMPORAL_JITTER".into()); } @@ -837,6 +878,10 @@ impl SpecializedMeshPipeline for MeshPipeline { }, )); + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHT_PROBES_IN_ARRAY".into()); + } + let format = if key.contains(MeshPipelineKey::HDR) { ViewTarget::TEXTURE_FORMAT_HDR } else { @@ -921,36 +966,44 @@ pub struct MeshBindGroups { model_only: Option, skinned: Option, morph_targets: HashMap, BindGroup>, + lightmaps: HashMap, BindGroup>, } impl MeshBindGroups { pub fn reset(&mut self) { self.model_only = None; self.skinned = None; self.morph_targets.clear(); + self.lightmaps.clear(); } - /// Get the `BindGroup` for `GpuMesh` with given `handle_id`. + /// Get the `BindGroup` for `GpuMesh` with given `handle_id` and lightmap + /// key `lightmap`. pub fn get( &self, asset_id: AssetId, + lightmap: Option>, is_skinned: bool, morph: bool, ) -> Option<&BindGroup> { - match (is_skinned, morph) { - (_, true) => self.morph_targets.get(&asset_id), - (true, false) => self.skinned.as_ref(), - (false, false) => self.model_only.as_ref(), + match (is_skinned, morph, lightmap) { + (_, true, _) => self.morph_targets.get(&asset_id), + (true, false, _) => self.skinned.as_ref(), + (false, false, Some(lightmap)) => self.lightmaps.get(&lightmap), + (false, false, None) => self.model_only.as_ref(), } } } +#[allow(clippy::too_many_arguments)] pub fn prepare_mesh_bind_group( meshes: Res>, + images: Res>, mut groups: ResMut, mesh_pipeline: Res, render_device: Res, mesh_uniforms: Res>, skins_uniform: Res, weights_uniform: Res, + render_lightmaps: Res, ) { groups.reset(); let layouts = &mesh_pipeline.mesh_layouts; @@ -976,25 +1029,35 @@ pub fn prepare_mesh_bind_group( } } } + + // Create lightmap bindgroups. + for &image_id in &render_lightmaps.all_lightmap_images { + if let (Entry::Vacant(entry), Some(image)) = + (groups.lightmaps.entry(image_id), images.get(image_id)) + { + entry.insert(layouts.lightmapped(&render_device, &model, image)); + } + } } pub struct SetMeshViewBindGroup; impl RenderCommand

for SetMeshViewBindGroup { type Param = (); - type ViewData = ( + type ViewQuery = ( Read, Read, Read, + Read, Read, ); - type ItemData = (); + type ItemQuery = (); #[inline] fn render<'w>( _item: &P, - (view_uniform, view_lights, view_fog, mesh_view_bind_group): ROQueryItem< + (view_uniform, view_lights, view_fog, view_light_probes, mesh_view_bind_group): ROQueryItem< 'w, - Self::ViewData, + Self::ViewQuery, >, _entity: (), _: SystemParamItem<'w, '_, Self::Param>, @@ -1003,7 +1066,12 @@ impl RenderCommand

for SetMeshViewBindGroup pass.set_bind_group( I, &mesh_view_bind_group.value, - &[view_uniform.offset, view_lights.offset, view_fog.offset], + &[ + view_uniform.offset, + view_lights.offset, + view_fog.offset, + **view_light_probes, + ], ); RenderCommandResult::Success @@ -1017,16 +1085,17 @@ impl RenderCommand

for SetMeshBindGroup { SRes, SRes, SRes, + SRes, ); - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( item: &P, _view: (), _item_query: (), - (bind_groups, mesh_instances, skin_indices, morph_indices): SystemParamItem< + (bind_groups, mesh_instances, skin_indices, morph_indices, lightmaps): SystemParamItem< 'w, '_, Self::Param, @@ -1049,10 +1118,17 @@ impl RenderCommand

for SetMeshBindGroup { let is_skinned = skin_index.is_some(); let is_morphed = morph_index.is_some(); - let Some(bind_group) = bind_groups.get(mesh.mesh_asset_id, is_skinned, is_morphed) else { + let lightmap = lightmaps + .render_lightmaps + .get(entity) + .map(|render_lightmap| render_lightmap.image); + + let Some(bind_group) = + bind_groups.get(mesh.mesh_asset_id, lightmap, is_skinned, is_morphed) + else { error!( "The MeshBindGroups resource wasn't set in the render phase. \ - It should be set by the queue_mesh_bind_group system.\n\ + It should be set by the prepare_mesh_bind_group system.\n\ This is a bevy bug! Please open an issue." ); return RenderCommandResult::Failure; @@ -1081,8 +1157,8 @@ impl RenderCommand

for SetMeshBindGroup { pub struct DrawMesh; impl RenderCommand

for DrawMesh { type Param = (SRes>, SRes); - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( item: &P, diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index e2a8041433c34..651de128cd7c8 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -68,6 +68,10 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { out.uv = vertex.uv; #endif +#ifdef VERTEX_UVS_B + out.uv_b = vertex.uv_b; +#endif + #ifdef VERTEX_TANGENTS out.world_tangent = mesh_functions::mesh_tangent_local_to_world( model, diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index f273da7bcb245..60beb9911530a 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -1,7 +1,9 @@ //! Bind group layout related definitions for the mesh pipeline. use bevy_math::Mat4; -use bevy_render::{mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice}; +use bevy_render::{ + mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice, texture::GpuImage, +}; use crate::render::skin::MAX_JOINTS; @@ -17,9 +19,9 @@ mod layout_entry { use crate::MeshUniform; use bevy_render::{ render_resource::{ - binding_types::{texture_3d, uniform_buffer_sized}, - BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, ShaderStages, - TextureSampleType, + binding_types::{sampler, texture_2d, texture_3d, uniform_buffer_sized}, + BindGroupLayoutEntryBuilder, BufferSize, GpuArrayBuffer, SamplerBindingType, + ShaderStages, TextureSampleType, }, renderer::RenderDevice, }; @@ -37,6 +39,12 @@ mod layout_entry { pub(super) fn targets() -> BindGroupLayoutEntryBuilder { texture_3d(TextureSampleType::Float { filterable: false }) } + pub(super) fn lightmaps_texture_view() -> BindGroupLayoutEntryBuilder { + texture_2d(TextureSampleType::Float { filterable: true }).visibility(ShaderStages::FRAGMENT) + } + pub(super) fn lightmaps_sampler() -> BindGroupLayoutEntryBuilder { + sampler(SamplerBindingType::Filtering).visibility(ShaderStages::FRAGMENT) + } } /// Individual [`BindGroupEntry`] @@ -44,7 +52,7 @@ mod layout_entry { mod entry { use super::{JOINT_BUFFER_SIZE, MORPH_BUFFER_SIZE}; use bevy_render::render_resource::{ - BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, TextureView, + BindGroupEntry, BindingResource, Buffer, BufferBinding, BufferSize, Sampler, TextureView, }; fn entry(binding: u32, size: u64, buffer: &Buffer) -> BindGroupEntry { @@ -72,6 +80,18 @@ mod entry { resource: BindingResource::TextureView(texture), } } + pub(super) fn lightmaps_texture_view(binding: u32, texture: &TextureView) -> BindGroupEntry { + BindGroupEntry { + binding, + resource: BindingResource::TextureView(texture), + } + } + pub(super) fn lightmaps_sampler(binding: u32, sampler: &Sampler) -> BindGroupEntry { + BindGroupEntry { + binding, + resource: BindingResource::Sampler(sampler), + } + } } /// All possible [`BindGroupLayout`]s in bevy's default mesh shader (`mesh.wgsl`). @@ -80,6 +100,9 @@ pub struct MeshLayouts { /// The mesh model uniform (transform) and nothing else. pub model_only: BindGroupLayout, + /// Includes the lightmap texture and uniform. + pub lightmapped: BindGroupLayout, + /// Also includes the uniform for skinning pub skinned: BindGroupLayout, @@ -102,6 +125,7 @@ impl MeshLayouts { pub fn new(render_device: &RenderDevice) -> Self { MeshLayouts { model_only: Self::model_only_layout(render_device), + lightmapped: Self::lightmapped_layout(render_device), skinned: Self::skinned_layout(render_device), morphed: Self::morphed_layout(render_device), morphed_skinned: Self::morphed_skinned_layout(render_device), @@ -158,6 +182,19 @@ impl MeshLayouts { ), ) } + fn lightmapped_layout(render_device: &RenderDevice) -> BindGroupLayout { + render_device.create_bind_group_layout( + "lightmapped_mesh_layout", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX, + ( + (0, layout_entry::model(render_device)), + (4, layout_entry::lightmaps_texture_view()), + (5, layout_entry::lightmaps_sampler()), + ), + ), + ) + } // ---------- BindGroup methods ---------- @@ -168,6 +205,22 @@ impl MeshLayouts { &[entry::model(0, model.clone())], ) } + pub fn lightmapped( + &self, + render_device: &RenderDevice, + model: &BindingResource, + lightmap: &GpuImage, + ) -> BindGroup { + render_device.create_bind_group( + "lightmapped_mesh_bind_group", + &self.lightmapped, + &[ + entry::model(0, model.clone()), + entry::lightmaps_texture_view(4, &lightmap.texture_view), + entry::lightmaps_sampler(5, &lightmap.sampler), + ], + ) + } pub fn skinned( &self, render_device: &RenderDevice, diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 0da870acfe6e7..89b73be2bd6f1 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -5,6 +5,7 @@ struct Mesh { // Use bevy_render::maths::affine_to_square to unpack model: mat3x4, previous_model: mat3x4, + lightmap_uv_rect: vec2, // 3x3 matrix packed in mat2x4 and f32 as: // [0].xyz, [1].x, // [1].yz, [2].xy diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 61140e4377677..e43e1e85f5689 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -17,7 +17,7 @@ use bevy_render::{ render_asset::RenderAssets, render_resource::{binding_types::*, *}, renderer::RenderDevice, - texture::{BevyDefault, FallbackImageCubemap, FallbackImageMsaa, FallbackImageZero, Image}, + texture::{BevyDefault, FallbackImage, FallbackImageMsaa, FallbackImageZero, Image}, view::{Msaa, ViewUniform, ViewUniforms}, }; @@ -27,9 +27,10 @@ use bevy_render::render_resource::binding_types::texture_cube; use bevy_render::render_resource::binding_types::{texture_2d_array, texture_cube_array}; use crate::{ - environment_map, prepass, EnvironmentMapLight, FogMeta, GlobalLightMeta, GpuFog, GpuLights, - GpuPointLights, LightMeta, MeshPipeline, MeshPipelineKey, ScreenSpaceAmbientOcclusionTextures, - ShadowSamplers, ViewClusterBindings, ViewShadowBindings, + environment_map::{self, RenderViewBindGroupEntries, RenderViewEnvironmentMaps}, + prepass, FogMeta, GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, + LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, + ScreenSpaceAmbientOcclusionTextures, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, }; #[derive(Clone)] @@ -49,11 +50,11 @@ bitflags::bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct MeshPipelineViewLayoutKey: u32 { - const MULTISAMPLED = (1 << 0); - const DEPTH_PREPASS = (1 << 1); - const NORMAL_PREPASS = (1 << 2); - const MOTION_VECTOR_PREPASS = (1 << 3); - const DEFERRED_PREPASS = (1 << 4); + const MULTISAMPLED = 1 << 0; + const DEPTH_PREPASS = 1 << 1; + const NORMAL_PREPASS = 1 << 2; + const MOTION_VECTOR_PREPASS = 1 << 3; + const DEFERRED_PREPASS = 1 << 4; } } @@ -166,6 +167,7 @@ fn buffer_layout( fn layout_entries( clustered_forward_buffer_binding_type: BufferBindingType, layout_key: MeshPipelineViewLayoutKey, + render_device: &RenderDevice, ) -> Vec { let mut entries = DynamicBindGroupLayoutEntries::new_with_indices( ShaderStages::FRAGMENT, @@ -234,27 +236,29 @@ fn layout_entries( (9, uniform_buffer::(false)), // Fog (10, uniform_buffer::(true)), + // Light probes + (11, uniform_buffer::(true)), // Screen space ambient occlusion texture ( - 11, + 12, texture_2d(TextureSampleType::Float { filterable: false }), ), ), ); // EnvironmentMapLight - let environment_map_entries = environment_map::get_bind_group_layout_entries(); + let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (12, environment_map_entries[0]), - (13, environment_map_entries[1]), - (14, environment_map_entries[2]), + (13, environment_map_entries[0]), + (14, environment_map_entries[1]), + (15, environment_map_entries[2]), )); // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (15, tonemapping_lut_entries[0]), - (16, tonemapping_lut_entries[1]), + (16, tonemapping_lut_entries[0]), + (17, tonemapping_lut_entries[1]), )); // Prepass @@ -264,7 +268,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([17, 18, 19, 20]) + .zip([18, 19, 20, 21]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -275,10 +279,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 21, + 22, texture_2d(TextureSampleType::Float { filterable: true }), ), - (22, sampler(SamplerBindingType::Filtering)), + (23, sampler(SamplerBindingType::Filtering)), )); entries.to_vec() @@ -292,7 +296,7 @@ pub fn generate_view_layouts( ) -> [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT] { array::from_fn(|i| { let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); - let entries = layout_entries(clustered_forward_buffer_binding_type, key); + let entries = layout_entries(clustered_forward_buffer_binding_type, key, render_device); #[cfg(debug_assertions)] let texture_count: usize = entries @@ -331,18 +335,19 @@ pub fn prepare_mesh_view_bind_groups( Option<&ScreenSpaceAmbientOcclusionTextures>, Option<&ViewPrepassTextures>, Option<&ViewTransmissionTexture>, - Option<&EnvironmentMapLight>, &Tonemapping, + Option<&RenderViewEnvironmentMaps>, )>, - (images, mut fallback_images, fallback_cubemap, fallback_image_zero): ( + (images, mut fallback_images, fallback_image, fallback_image_zero): ( Res>, FallbackImageMsaa, - Res, + Res, Res, ), msaa: Res, globals_buffer: Res, tonemapping_luts: Res, + light_probes_buffer: Res, ) { if let ( Some(view_binding), @@ -350,12 +355,14 @@ pub fn prepare_mesh_view_bind_groups( Some(point_light_binding), Some(globals), Some(fog_binding), + Some(light_probes_binding), ) = ( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), global_light_meta.gpu_point_lights.binding(), globals_buffer.buffer.binding(), fog_meta.gpu_fogs.binding(), + light_probes_buffer.binding(), ) { for ( entity, @@ -364,8 +371,8 @@ pub fn prepare_mesh_view_bind_groups( ssao_textures, prepass_textures, transmission_texture, - environment_map, tonemapping, + render_view_environment_maps, ) in &views { let fallback_ssao = fallback_images @@ -393,19 +400,44 @@ pub fn prepare_mesh_view_bind_groups( (8, cluster_bindings.offsets_and_counts_binding().unwrap()), (9, globals.clone()), (10, fog_binding.clone()), - (11, ssao_view), + (11, light_probes_binding.clone()), + (12, ssao_view), )); - let env_map_bindings = - environment_map::get_bindings(environment_map, &images, &fallback_cubemap); - entries = entries.extend_with_indices(( - (12, env_map_bindings.0), - (13, env_map_bindings.1), - (14, env_map_bindings.2), - )); + let bind_group_entries = RenderViewBindGroupEntries::get( + render_view_environment_maps, + &images, + &fallback_image, + &render_device, + ); + + match bind_group_entries { + RenderViewBindGroupEntries::Single { + diffuse_texture_view, + specular_texture_view, + sampler, + } => { + entries = entries.extend_with_indices(( + (13, diffuse_texture_view), + (14, specular_texture_view), + (15, sampler), + )); + } + RenderViewBindGroupEntries::Multiple { + ref diffuse_texture_views, + ref specular_texture_views, + sampler, + } => { + entries = entries.extend_with_indices(( + (13, diffuse_texture_views.as_slice()), + (14, specular_texture_views.as_slice()), + (15, sampler), + )); + } + } let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping); - entries = entries.extend_with_indices(((15, lut_bindings.0), (16, lut_bindings.1))); + entries = entries.extend_with_indices(((16, lut_bindings.0), (17, lut_bindings.1))); // When using WebGL, we can't have a depth texture with multisampling let prepass_bindings; @@ -415,7 +447,7 @@ pub fn prepare_mesh_view_bind_groups( for (binding, index) in prepass_bindings .iter() .map(Option::as_ref) - .zip([17, 18, 19, 20]) + .zip([18, 19, 20, 21]) .flat_map(|(b, i)| b.map(|b| (b, i))) { entries = entries.extend_with_indices(((index, binding),)); @@ -431,7 +463,7 @@ pub fn prepare_mesh_view_bind_groups( .unwrap_or(&fallback_image_zero.sampler); entries = - entries.extend_with_indices(((21, transmission_view), (22, transmission_sampler))); + entries.extend_with_indices(((22, transmission_view), (23, transmission_sampler))); commands.entity(entity).insert(MeshViewBindGroup { value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 6f4293d6d61ca..973ff4bca696e 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -33,44 +33,50 @@ @group(0) @binding(9) var globals: Globals; @group(0) @binding(10) var fog: types::Fog; +@group(0) @binding(11) var light_probes: types::LightProbes; -@group(0) @binding(11) var screen_space_ambient_occlusion_texture: texture_2d; +@group(0) @binding(12) var screen_space_ambient_occlusion_texture: texture_2d; -@group(0) @binding(12) var environment_map_diffuse: texture_cube; -@group(0) @binding(13) var environment_map_specular: texture_cube; -@group(0) @binding(14) var environment_map_sampler: sampler; +#ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY +@group(0) @binding(13) var diffuse_environment_maps: binding_array, 8u>; +@group(0) @binding(14) var specular_environment_maps: binding_array, 8u>; +#else +@group(0) @binding(13) var diffuse_environment_map: texture_cube; +@group(0) @binding(14) var specular_environment_map: texture_cube; +#endif +@group(0) @binding(15) var environment_map_sampler: sampler; -@group(0) @binding(15) var dt_lut_texture: texture_3d; -@group(0) @binding(16) var dt_lut_sampler: sampler; +@group(0) @binding(16) var dt_lut_texture: texture_3d; +@group(0) @binding(17) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(17) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(18) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(18) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(19) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(19) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(20) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(17) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(18) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(18) var normal_prepass_texture: texture_2d; +@group(0) @binding(19) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(19) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(20) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(20) var deferred_prepass_texture: texture_2d; +@group(0) @binding(21) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(21) var view_transmission_texture: texture_2d; -@group(0) @binding(22) var view_transmission_sampler: sampler; +@group(0) @binding(22) var view_transmission_texture: texture_2d; +@group(0) @binding(23) var view_transmission_sampler: sampler; diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 3062ad671a77f..04fd0d09eea1c 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -58,6 +58,7 @@ struct Lights { n_directional_lights: u32, spot_light_shadowmap_offset: i32, environment_map_smallest_specular_mip_level: u32, + environment_map_intensity: f32, }; struct Fog { @@ -109,3 +110,25 @@ struct ClusterOffsetsAndCounts { data: array, 1024u>, }; #endif + +struct ReflectionProbe { + // This is stored as the transpose in order to save space in this structure. + // It'll be transposed in the `environment_map_light` function. + inverse_transpose_transform: mat3x4, + cubemap_index: i32, + intensity: f32, +}; + +struct LightProbes { + // This must match `MAX_VIEW_REFLECTION_PROBES` on the Rust side. + reflection_probes: array, + reflection_probe_count: i32, + // The index of the view environment map cubemap binding, or -1 if there's + // no such cubemap. + view_cubemap_index: i32, + // The smallest valid mipmap level for the specular environment cubemap + // associated with the view. + smallest_specular_mip_level_for_view: u32, + // The intensity of the environment map associated with the view. + intensity_for_view: f32, +}; diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 6472f80d81524..169d42f0a6f81 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -5,9 +5,11 @@ pbr_bindings, pbr_types, prepass_utils, + lighting, mesh_bindings::mesh, mesh_view_bindings::view, parallax_mapping::parallaxed_uv, + lightmap::lightmap, } #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION @@ -67,6 +69,9 @@ fn pbr_input_from_standard_material( pbr_input.material.base_color *= pbr_bindings::material.base_color; pbr_input.material.deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id; + // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" + let NdotV = max(dot(pbr_input.N, pbr_input.V), 0.0001); + #ifdef VERTEX_UVS var uv = in.uv; @@ -119,6 +124,7 @@ fn pbr_input_from_standard_material( // metallic and perceptual roughness var metallic: f32 = pbr_bindings::material.metallic; var perceptual_roughness: f32 = pbr_bindings::material.perceptual_roughness; + let roughness = lighting::perceptualRoughnessToRoughness(perceptual_roughness); #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT) != 0u) { let metallic_roughness = textureSampleBias(pbr_bindings::metallic_roughness_texture, pbr_bindings::metallic_roughness_sampler, uv, view.mip_bias); @@ -158,20 +164,23 @@ fn pbr_input_from_standard_material( #endif pbr_input.material.diffuse_transmission = diffuse_transmission; - // occlusion - // TODO: Split into diffuse/specular occlusion? - var occlusion: vec3 = vec3(1.0); + var diffuse_occlusion: vec3 = vec3(1.0); + var specular_occlusion: f32 = 1.0; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT) != 0u) { - occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r); + diffuse_occlusion = vec3(textureSampleBias(pbr_bindings::occlusion_texture, pbr_bindings::occlusion_sampler, uv, view.mip_bias).r); } #endif #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); - occlusion = min(occlusion, ssao_multibounce); + diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce); + // Use SSAO to estimate the specular occlusion. + // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" + specular_occlusion = saturate(pow(NdotV + ssao, exp2(-16.0 * roughness - 1.0)) - 1.0 + ssao); #endif - pbr_input.occlusion = occlusion; + pbr_input.diffuse_occlusion = diffuse_occlusion; + pbr_input.specular_occlusion = specular_occlusion; // N (normal vector) #ifndef LOAD_PREPASS_NORMALS @@ -191,6 +200,13 @@ fn pbr_input_from_standard_material( view.mip_bias, ); #endif + +#ifdef LIGHTMAP + pbr_input.lightmap_light = lightmap( + in.uv_b, + pbr_bindings::material.lightmap_exposure, + in.instance_index); +#endif } return pbr_input; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 66d815db7ec4a..eca7463d9b178 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -164,7 +164,8 @@ fn apply_pbr_lighting( let specular_transmissive_color = specular_transmission * in.material.base_color.rgb; - let occlusion = in.occlusion; + let diffuse_occlusion = in.diffuse_occlusion; + let specular_occlusion = in.specular_occlusion; // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" let NdotV = max(dot(in.N, in.V), 0.0001); @@ -306,7 +307,7 @@ fn apply_pbr_lighting( } // Ambient light (indirect) - var indirect_light = ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, occlusion); + var indirect_light = ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); if diffuse_transmission > 0.0 { // NOTE: We use the diffuse transmissive color, the second Lambertian lobe's calculated @@ -316,14 +317,23 @@ fn apply_pbr_lighting( // perceptual_roughness = 1.0; // NdotV = 1.0; // F0 = vec3(0.0) - // occlusion = vec3(1.0) + // diffuse_occlusion = vec3(1.0) transmitted_light += ambient::ambient_light(diffuse_transmissive_lobe_world_position, -in.N, -in.V, 1.0, diffuse_transmissive_color, vec3(0.0), 1.0, vec3(1.0)); } // Environment map light (indirect) #ifdef ENVIRONMENT_MAP - let environment_light = environment_map::environment_map_light(perceptual_roughness, roughness, diffuse_color, NdotV, f_ab, in.N, R, F0); - indirect_light += (environment_light.diffuse * occlusion) + environment_light.specular; + let environment_light = environment_map::environment_map_light( + perceptual_roughness, + roughness, + diffuse_color, + NdotV, + f_ab, + in.N, + R, + F0, + in.world_position.xyz); + indirect_light += (environment_light.diffuse * diffuse_occlusion) + (environment_light.specular * specular_occlusion); // we'll use the specular component of the transmitted environment // light in the call to `specular_transmissive_light()` below @@ -338,7 +348,7 @@ fn apply_pbr_lighting( // NdotV = 1.0; // R = T // see definition below // F0 = vec3(1.0) - // occlusion = 1.0 + // diffuse_occlusion = 1.0 // // (This one is slightly different from the other light types above, because the environment // map light returns both diffuse and specular components separately, and we want to use both) @@ -348,7 +358,16 @@ fn apply_pbr_lighting( refract(in.V, -in.N, 1.0 / ior) * thickness // add refracted vector scaled by thickness, towards exit point ); // normalize to find exit point view vector - let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light(perceptual_roughness, roughness, vec3(1.0), 1.0, f_ab, -in.N, T, vec3(1.0)); + let transmitted_environment_light = bevy_pbr::environment_map::environment_map_light( + perceptual_roughness, + roughness, + vec3(1.0), + 1.0, + f_ab, + -in.N, + T, + vec3(1.0), + in.world_position.xyz); transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; specular_transmitted_environment_light = transmitted_environment_light.specular * specular_transmissive_color; } @@ -358,6 +377,10 @@ fn apply_pbr_lighting( let specular_transmitted_environment_light = vec3(0.0); #endif +#ifdef LIGHTMAP + indirect_light += in.lightmap_light * diffuse_color; +#endif + let emissive_light = emissive.rgb * output_color.a; if specular_transmission > 0.0 { @@ -381,7 +404,7 @@ fn apply_pbr_lighting( // Total light output_color = vec4( - transmitted_light + direct_light + indirect_light + emissive_light, + view_bindings::view.exposure * (transmitted_light + direct_light + indirect_light + emissive_light), output_color.a ); @@ -418,7 +441,7 @@ fn apply_fog(fog_params: mesh_view_types::Fog, input_color: vec4, fragment_ 0.0 ), fog_params.directional_light_exponent - ) * light.color.rgb; + ) * light.color.rgb * view_bindings::view.exposure; } } diff --git a/crates/bevy_pbr/src/render/pbr_transmission.wgsl b/crates/bevy_pbr/src/render/pbr_transmission.wgsl index f13e09920d4d7..65f84c86bbcab 100644 --- a/crates/bevy_pbr/src/render/pbr_transmission.wgsl +++ b/crates/bevy_pbr/src/render/pbr_transmission.wgsl @@ -42,6 +42,9 @@ fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, background_color = fetch_transmissive_background(offset_position, frag_coord, view_z, perceptual_roughness); } + // Compensate for exposure, since the background color is coming from an already exposure-adjusted texture + background_color = vec4(background_color.rgb / view_bindings::view.exposure, background_color.a); + // Dot product of the refracted direction with the exit normal (Note: We assume the exit normal is the entry normal but inverted) let MinusNdotT = dot(-N, T); diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index ff2fe8801369e..cd7170275b2f5 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -17,6 +17,7 @@ struct StandardMaterial { alpha_cutoff: f32, parallax_depth_scale: f32, max_parallax_layer_count: f32, + lightmap_exposure: f32, max_relief_mapping_search_steps: u32, /// ID for specifying which deferred lighting pass should be used for rendering this material, if any. deferred_lighting_pass_id: u32, @@ -79,7 +80,8 @@ fn standard_material_new() -> StandardMaterial { struct PbrInput { material: StandardMaterial, - occlusion: vec3, + diffuse_occlusion: vec3, + specular_occlusion: f32, frag_coord: vec4, world_position: vec4, // Normalized world normal used for shadow mapping as normal-mapping is not used for shadow @@ -90,6 +92,7 @@ struct PbrInput { // Normalized view vector in world space, pointing from the fragment world position toward the // view world position V: vec3, + lightmap_light: vec3, is_orthographic: bool, flags: u32, }; @@ -99,7 +102,8 @@ fn pbr_input_new() -> PbrInput { var pbr_input: PbrInput; pbr_input.material = standard_material_new(); - pbr_input.occlusion = vec3(1.0); + pbr_input.diffuse_occlusion = vec3(1.0); + pbr_input.specular_occlusion = 1.0; pbr_input.frag_coord = vec4(0.0, 0.0, 0.0, 1.0); pbr_input.world_position = vec4(0.0, 0.0, 0.0, 1.0); @@ -110,6 +114,8 @@ fn pbr_input_new() -> PbrInput { pbr_input.N = vec3(0.0, 0.0, 1.0); pbr_input.V = vec3(1.0, 0.0, 0.0); + pbr_input.lightmap_light = vec3(0.0); + pbr_input.flags = 0u; return pbr_input; diff --git a/crates/bevy_pbr/src/ssao/gtao.wgsl b/crates/bevy_pbr/src/ssao/gtao.wgsl index a735fef2bc349..be5fea01ee230 100644 --- a/crates/bevy_pbr/src/ssao/gtao.wgsl +++ b/crates/bevy_pbr/src/ssao/gtao.wgsl @@ -139,7 +139,8 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { s *= s; // https://github.com/GameTechDev/XeGTAO#sample-distribution let sample = s * sample_mul; - let sample_mip_level = clamp(log2(length(sample)) - 3.3, 0.0, 5.0); // https://github.com/GameTechDev/XeGTAO#memory-bandwidth-bottleneck + // * view.viewport.zw gets us from [0, 1] to [0, viewport_size], which is needed for this to get the correct mip levels + let sample_mip_level = clamp(log2(length(sample * view.viewport.zw)) - 3.3, 0.0, 5.0); // https://github.com/GameTechDev/XeGTAO#memory-bandwidth-bottleneck let sample_position_1 = load_and_reconstruct_view_space_position(uv + sample, sample_mip_level); let sample_position_2 = load_and_reconstruct_view_space_position(uv - sample, sample_mip_level); diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index a7ba6e1aadf73..d9c9b8dc0fea1 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -7,7 +7,7 @@ use bevy_core_pipeline::{ }; use bevy_ecs::{ prelude::{Bundle, Component, Entity}, - query::{QueryItem, With}, + query::{Has, QueryItem, With}, reflect::ReflectComponent, schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, @@ -612,7 +612,7 @@ fn prepare_ssao_pipelines( views: Query<( Entity, &ScreenSpaceAmbientOcclusionSettings, - Option<&TemporalJitter>, + Has, )>, ) { for (entity, ssao_settings, temporal_jitter) in &views { @@ -621,7 +621,7 @@ fn prepare_ssao_pipelines( &pipeline, SsaoPipelineKey { ssao_settings: ssao_settings.clone(), - temporal_jitter: temporal_jitter.is_some(), + temporal_jitter, }, ); @@ -681,7 +681,7 @@ fn prepare_ssao_bind_groups( "ssao_preprocess_depth_bind_group", &pipelines.preprocess_depth_bind_group_layout, &BindGroupEntries::sequential(( - &prepass_textures.depth.as_ref().unwrap().default_view, + prepass_textures.depth_view().unwrap(), &create_depth_view(0), &create_depth_view(1), &create_depth_view(2), @@ -695,7 +695,7 @@ fn prepare_ssao_bind_groups( &pipelines.gtao_bind_group_layout, &BindGroupEntries::sequential(( &ssao_textures.preprocessed_depth_texture.default_view, - &prepass_textures.normal.as_ref().unwrap().default_view, + prepass_textures.normal_view().unwrap(), &pipelines.hilbert_index_lut, &ssao_textures.ssao_noisy_texture.default_view, &ssao_textures.depth_differences_texture.default_view, diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index dc1075090c891..e779a1668d2f9 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -43,10 +43,10 @@ impl Plugin for WireframePlugin { .add_systems( Update, ( - global_color_changed.run_if(resource_changed::()), + global_color_changed.run_if(resource_changed::), wireframe_color_changed, apply_wireframe_material, - apply_global_wireframe_material.run_if(resource_changed::()), + apply_global_wireframe_material.run_if(resource_changed::), ), ); } diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 1099aeb479e26..4202293037d1b 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" default = [] # When enabled, provides Bevy-related reflection implementations bevy = ["glam", "smallvec", "bevy_math", "smol_str"] +smallvec = [] # When enabled, allows documentation comments to be accessed via reflection documentation = ["bevy_reflect_derive/documentation"] @@ -30,12 +31,8 @@ erased-serde = "0.3" downcast-rs = "1.2" thiserror = "1.0" serde = "1" -smallvec = { version = "1.6", features = [ - "serde", - "union", - "const_generics", -], optional = true } -glam = { version = "0.24.1", features = ["serde"], optional = true } + +glam = { version = "0.25", features = ["serde"], optional = true } smol_str = { version = "0.2.0", optional = true } [dev-dependencies] @@ -44,6 +41,7 @@ rmp-serde = "1.1" bincode = "1.3" serde_json = "1.0" serde = { version = "1", features = ["derive"] } +static_assertions = "1.1.0" [[example]] name = "reflect_docs" diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs b/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs index f3960df7c4bb3..ee388731d7497 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs @@ -168,9 +168,9 @@ impl TypePathAttrs { /// /// Registering the `Default` implementation: /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// // Import ReflectDefault so it's accessible by the derive macro -/// use bevy_reflect::prelude::ReflectDefault. +/// use bevy_reflect::prelude::ReflectDefault; /// /// #[derive(Reflect, Default)] /// #[reflect(Default)] @@ -179,7 +179,7 @@ impl TypePathAttrs { /// /// Registering the `Hash` implementation: /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// // `Hash` is a "special trait" and does not need (nor have) a ReflectHash struct /// /// #[derive(Reflect, Hash)] @@ -189,7 +189,7 @@ impl TypePathAttrs { /// /// Registering the `Hash` implementation using a custom function: /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// // This function acts as our `Hash` implementation and /// // corresponds to the `Reflect::reflect_hash` method. /// fn get_hash(foo: &Foo) -> Option { diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs b/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs index e013e68b77849..ce8777bbc041d 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs @@ -29,7 +29,7 @@ pub(crate) enum ReflectDerive<'a> { /// /// # Example /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// #[derive(Reflect)] /// // traits /// // |----------------------------------------| @@ -54,7 +54,7 @@ pub(crate) struct ReflectMeta<'a> { /// /// # Example /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// #[derive(Reflect)] /// #[reflect(PartialEq, Serialize, Deserialize, Default)] /// struct ThingThatImReflecting { @@ -73,7 +73,7 @@ pub(crate) struct ReflectStruct<'a> { /// /// # Example /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// #[derive(Reflect)] /// #[reflect(PartialEq, Serialize, Deserialize, Default)] /// enum ThingThatImReflecting { @@ -573,7 +573,7 @@ impl<'a> EnumVariant<'a> { /// /// # Example /// -/// ```rust,ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// # use syn::parse_quote; /// # use bevy_reflect_derive::ReflectTypePath; /// let path: syn::Path = parse_quote!(::core::marker::PhantomData)?; diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs b/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs index a733ec2e262bf..2d2bebfc9c08d 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs @@ -83,7 +83,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream }, ); - let type_path_impl = impl_type_path(reflect_enum.meta(), &where_clause_options); + let type_path_impl = impl_type_path(reflect_enum.meta()); let get_type_registration_impl = reflect_enum .meta() diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs b/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs index fb1565f0012d4..9aef44d3505a8 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs @@ -89,7 +89,7 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS }, ); - let type_path_impl = impl_type_path(reflect_struct.meta(), &where_clause_options); + let type_path_impl = impl_type_path(reflect_struct.meta()); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs index 7a097a1a1a465..14af4851fd2e9 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs @@ -82,7 +82,7 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: }, ); - let type_path_impl = impl_type_path(reflect_struct.meta(), &where_clause_options); + let type_path_impl = impl_type_path(reflect_struct.meta()); let (impl_generics, ty_generics, where_clause) = reflect_struct .meta() diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs b/crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs index 5894ef82e5540..46edd1895c3c5 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs @@ -48,10 +48,11 @@ pub(crate) enum TypedProperty { TypePath, } -pub(crate) fn impl_type_path( - meta: &ReflectMeta, - where_clause_options: &WhereClauseOptions, -) -> proc_macro2::TokenStream { +pub(crate) fn impl_type_path(meta: &ReflectMeta) -> proc_macro2::TokenStream { + // Use `WhereClauseOptions::new_value` here so we don't enforce reflection bounds, + // ensuring the impl applies in the most cases possible. + let where_clause_options = &WhereClauseOptions::new_value(meta); + if !meta.traits().type_path_attrs().should_auto_derive() { return proc_macro2::TokenStream::new(); } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs b/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs index a9f43cbbfede9..17e0838d799d3 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs @@ -31,7 +31,7 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { }, ); - let type_path_impl = impl_type_path(meta, &where_clause_options); + let type_path_impl = impl_type_path(meta); let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl(); let where_reflect_clause = extend_where_clause(where_clause, &where_clause_options); diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs b/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs index e87d3ccf5c8d3..d0d59f3655a15 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs @@ -39,7 +39,6 @@ use reflect_value::ReflectValueDef; use syn::spanned::Spanned; use syn::{parse_macro_input, DeriveInput}; use type_path::NamedTypePathDef; -use utility::WhereClauseOptions; pub(crate) static REFLECT_ATTRIBUTE_NAME: &str = "reflect"; pub(crate) static REFLECT_VALUE_ATTRIBUTE_NAME: &str = "reflect_value"; @@ -283,11 +282,7 @@ pub fn derive_type_path(input: TokenStream) -> TokenStream { Err(err) => return err.into_compile_error().into(), }; - let type_path_impl = impls::impl_type_path( - derive_data.meta(), - // Use `WhereClauseOptions::new_value` here so we don't enforce reflection bounds - &WhereClauseOptions::new_value(derive_data.meta()), - ); + let type_path_impl = impls::impl_type_path(derive_data.meta()); TokenStream::from(quote! { const _: () = { @@ -323,7 +318,7 @@ pub fn derive_type_uuid(input: TokenStream) -> TokenStream { /// /// # Example /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// # use std::any::TypeId; /// # use bevy_reflect_derive::{Reflect, reflect_trait}; /// #[reflect_trait] // Generates `ReflectMyTrait` @@ -377,20 +372,20 @@ pub fn reflect_trait(args: TokenStream, input: TokenStream) -> TokenStream { /// /// Types can be passed with or without registering type data: /// -/// ```ignore -/// impl_reflect_value!(::my_crate::Foo); -/// impl_reflect_value!(::my_crate::Bar(Debug, Default, Serialize, Deserialize)); +/// ```ignore (bevy_reflect is not accessible from this crate) +/// impl_reflect_value!(my_crate::Foo); +/// impl_reflect_value!(my_crate::Bar(Debug, Default, Serialize, Deserialize)); /// ``` /// /// Generic types can also specify their parameters and bounds: /// -/// ```ignore -/// impl_reflect_value!(::my_crate::Foo where T1: Bar (Default, Serialize, Deserialize)); +/// ```ignore (bevy_reflect is not accessible from this crate) +/// impl_reflect_value!(my_crate::Foo where T1: Bar (Default, Serialize, Deserialize)); /// ``` /// /// Custom type paths can be specified: /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_reflect_value!((in not_my_crate as NotFoo) Foo(Debug, Default)); /// ``` /// @@ -445,7 +440,7 @@ pub fn impl_reflect_value(input: TokenStream) -> TokenStream { /// /// # Example /// Implementing `Reflect` for `bevy::prelude::Vec3` as a struct type: -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// use bevy::prelude::Vec3; /// /// impl_reflect_struct!( @@ -520,7 +515,7 @@ pub fn impl_reflect_struct(input: TokenStream) -> TokenStream { /// /// # Examples /// -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_from_reflect_value!(foo where T1: Bar); /// ``` /// @@ -565,27 +560,27 @@ pub fn impl_from_reflect_value(input: TokenStream) -> TokenStream { /// # Examples /// /// Implementing `TypePath` on a foreign type: -/// ```rust,ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_type_path!(::foreign_crate::foo::bar::Baz); /// ``` /// /// On a generic type: -/// ```rust,ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_type_path!(::foreign_crate::Foo); /// ``` /// /// On a primitive (note this will not compile for a non-primitive type): -/// ```rust,ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_type_path!(bool); /// ``` /// /// With a custom type path: -/// ```rust,ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_type_path!((in other_crate::foo::bar) Baz); /// ``` /// /// With a custom type path and a custom type name: -/// ```rust,ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// impl_type_path!((in other_crate::foo as Baz) Bar); /// ``` /// @@ -613,7 +608,7 @@ pub fn impl_type_path(input: TokenStream) -> TokenStream { let meta = ReflectMeta::new(type_path, ReflectTraits::default()); - let type_path_impl = impls::impl_type_path(&meta, &WhereClauseOptions::new_value(&meta)); + let type_path_impl = impls::impl_type_path(&meta); TokenStream::from(quote! { const _: () = { diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs b/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs index b3d0f663a3a44..da06e78667bf7 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs @@ -10,7 +10,7 @@ use syn::{parenthesized, Attribute, Generics, Path}; /// /// This takes the form: /// -/// ```ignore +/// ```ignore (Method expecting TokenStream is better represented with raw tokens) /// // Standard /// ::my_crate::foo::Bar(TraitA, TraitB) /// diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs b/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs index 1adc5787e593b..7bce217f27424 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs @@ -18,7 +18,13 @@ pub(crate) fn get_bevy_reflect_path() -> Path { /// /// # Example /// -/// ```ignore +/// ``` +/// # use proc_macro2::Ident; +/// # // We can't import this method because of its visibility. +/// # fn get_reflect_ident(name: &str) -> Ident { +/// # let reflected = format!("Reflect{name}"); +/// # Ident::new(&reflected, proc_macro2::Span::call_site()) +/// # } /// let reflected: Ident = get_reflect_ident("Hash"); /// assert_eq!("ReflectHash", reflected.to_string()); /// ``` @@ -132,9 +138,9 @@ impl WhereClauseOptions { let custom_bounds = active_bounds(field).map(|bounds| quote!(+ #bounds)); let bounds = if is_from_reflect { - quote!(#bevy_reflect_path::FromReflect + #bevy_reflect_path::TypePath #custom_bounds) + quote!(#bevy_reflect_path::FromReflect #custom_bounds) } else { - quote!(#bevy_reflect_path::Reflect + #bevy_reflect_path::TypePath #custom_bounds) + quote!(#bevy_reflect_path::Reflect #custom_bounds) }; (ty, bounds) @@ -195,7 +201,7 @@ impl WhereClauseOptions { /// # Example /// /// The struct: -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// #[derive(Reflect)] /// struct Foo { /// a: T, @@ -206,7 +212,7 @@ impl WhereClauseOptions { /// will have active types: `[T]` and ignored types: `[U]` /// /// The `extend_where_clause` function will yield the following `where` clause: -/// ```ignore +/// ```ignore (bevy_reflect is not accessible from this crate) /// where /// T: Reflect, // active_trait_bounds /// U: Any + Send + Sync, // ignored_trait_bounds diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 1497aeb149f7d..82e6fb53d92f2 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -297,16 +297,16 @@ impl Reflect for DynamicArray { array_partial_eq(self, value) } - #[inline] - fn is_dynamic(&self) -> bool { - true - } - fn debug(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "DynamicArray(")?; array_debug(self, f)?; write!(f, ")") } + + #[inline] + fn is_dynamic(&self) -> bool { + true + } } impl Array for DynamicArray { diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 6ff89469afcff..491404a3df11a 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -1,5 +1,7 @@ use bevy_reflect_derive::impl_type_path; +use bevy_utils::smallvec; use smallvec::SmallVec; + use std::any::Any; use crate::utility::GenericTypeInfoCell; @@ -148,7 +150,7 @@ where } } -impl_type_path!(::smallvec::SmallVec); +impl_type_path!(::bevy_utils::smallvec::SmallVec); impl FromReflect for SmallVec where diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 4fd1043e31eed..5dcfa9e375289 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -80,6 +80,7 @@ impl_reflect_value!(isize( impl_reflect_value!(f32(Debug, PartialEq, Serialize, Deserialize, Default)); impl_reflect_value!(f64(Debug, PartialEq, Serialize, Deserialize, Default)); impl_type_path!(str); +impl_type_path!(::bevy_utils::EntityHash); impl_reflect_value!(::alloc::string::String( Debug, Hash, @@ -1162,23 +1163,6 @@ impl List for Cow<'static, [T]> { self.to_mut().get_mut(index).map(|x| x as &mut dyn Reflect) } - fn len(&self) -> usize { - self.as_ref().len() - } - - fn iter(&self) -> ListIter { - ListIter::new(self) - } - - fn drain(self: Box) -> Vec> { - // into_owned() is not unnecessary here because it avoids cloning whenever you have a Cow::Owned already - #[allow(clippy::unnecessary_to_owned)] - self.into_owned() - .into_iter() - .map(|value| value.clone_value()) - .collect() - } - fn insert(&mut self, index: usize, element: Box) { let value = element.take::().unwrap_or_else(|value| { T::from_reflect(&*value).unwrap_or_else(|| { @@ -1210,9 +1194,30 @@ impl List for Cow<'static, [T]> { .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(self: Box) -> Vec> { + // into_owned() is not unnecessary here because it avoids cloning whenever you have a Cow::Owned already + #[allow(clippy::unnecessary_to_owned)] + self.into_owned() + .into_iter() + .map(|value| value.clone_value()) + .collect() + } } impl Reflect for Cow<'static, [T]> { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + fn into_any(self: Box) -> Box { self } @@ -1269,10 +1274,6 @@ impl Reflect for Cow<'static, [T]> { fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { crate::list_partial_eq(self, value) } - - fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { - Some(::type_info()) - } } impl Typed for Cow<'static, [T]> { @@ -1295,7 +1296,7 @@ impl FromReflect for Cow<'static, [T]> { for field in ref_list.iter() { temp_vec.push(T::from_reflect(field)?); } - temp_vec.try_into().ok() + Some(temp_vec.into()) } else { None } @@ -1513,11 +1514,15 @@ mod tests { Enum, FromReflect, Reflect, ReflectSerialize, TypeInfo, TypeRegistry, Typed, VariantInfo, VariantType, }; - use bevy_utils::HashMap; use bevy_utils::{Duration, Instant}; + use bevy_utils::{EntityHashMap, HashMap}; + use static_assertions::assert_impl_all; use std::f32::consts::{PI, TAU}; use std::path::Path; + // EntityHashMap should implement Reflect + assert_impl_all!(EntityHashMap: Reflect); + #[test] fn can_serialize_duration() { let mut type_registry = TypeRegistry::default(); @@ -1598,6 +1603,8 @@ mod tests { #[test] fn option_should_impl_enum() { + assert_impl_all!(Option<()>: Enum); + let mut value = Some(123usize); assert!(value @@ -1671,6 +1678,8 @@ mod tests { #[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 { @@ -1701,6 +1710,7 @@ mod tests { panic!("Expected `TypeInfo::Enum`"); } } + #[test] fn nonzero_usize_impl_reflect_from_reflect() { let a: &dyn Reflect = &std::num::NonZeroUsize::new(42).unwrap(); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 3eb719b5548bb..231ab7d0e0c58 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1383,6 +1383,7 @@ mod tests { // List (SmallVec) #[cfg(feature = "smallvec")] { + use bevy_utils::smallvec; type MySmallVec = smallvec::SmallVec<[String; 2]>; let info = MySmallVec::type_info(); @@ -1556,7 +1557,7 @@ mod tests { /// /// # Example /// - /// ```ignore + /// ```ignore (This is only used for a unit test, no need to doc test) /// let some_struct = SomeStruct; /// ``` #[derive(Reflect)] @@ -1899,6 +1900,10 @@ bevy_reflect::tests::Test { }) } + fn type_ident() -> Option<&'static str> { + Some("Foo") + } + fn crate_name() -> Option<&'static str> { Some("bevy_reflect") } @@ -1906,10 +1911,6 @@ bevy_reflect::tests::Test { fn module_path() -> Option<&'static str> { Some("bevy_reflect::tests") } - - fn type_ident() -> Option<&'static str> { - Some("Foo") - } } // Can use `TypePath` diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 3d4e8e58e6784..34e41bf5ddb6a 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -248,10 +248,30 @@ impl Map for DynamicMap { .map(move |index| &mut *self.values.get_mut(index).unwrap().1) } + fn get_at(&self, index: usize) -> Option<(&dyn Reflect, &dyn Reflect)> { + self.values + .get(index) + .map(|(key, value)| (&**key, &**value)) + } + + fn get_at_mut(&mut self, index: usize) -> Option<(&dyn Reflect, &mut dyn Reflect)> { + self.values + .get_mut(index) + .map(|(key, value)| (&**key, &mut **value)) + } + fn len(&self) -> usize { self.values.len() } + fn iter(&self) -> MapIter { + MapIter::new(self) + } + + fn drain(self: Box) -> Vec<(Box, Box)> { + self.values + } + fn clone_dynamic(&self) -> DynamicMap { DynamicMap { represented_type: self.represented_type, @@ -264,22 +284,6 @@ impl Map for DynamicMap { } } - fn iter(&self) -> MapIter { - MapIter::new(self) - } - - fn get_at(&self, index: usize) -> Option<(&dyn Reflect, &dyn Reflect)> { - self.values - .get(index) - .map(|(key, value)| (&**key, &**value)) - } - - fn get_at_mut(&mut self, index: usize) -> Option<(&dyn Reflect, &mut dyn Reflect)> { - self.values - .get_mut(index) - .map(|(key, value)| (&**key, &mut **value)) - } - fn insert_boxed( &mut self, key: Box, @@ -306,10 +310,6 @@ impl Map for DynamicMap { let (_key, value) = self.values.remove(index); Some(value) } - - fn drain(self: Box) -> Vec<(Box, Box)> { - self.values - } } impl Reflect for DynamicMap { diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index a1ceb885f2b56..6212a74d787b0 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -49,14 +49,14 @@ impl StructLikeInfo for StructInfo { self.type_path() } - fn field_at(&self, index: usize) -> Option<&NamedField> { - self.field_at(index) - } - fn get_field(&self, name: &str) -> Option<&NamedField> { self.field(name) } + fn field_at(&self, index: usize) -> Option<&NamedField> { + self.field_at(index) + } + fn get_field_len(&self) -> usize { self.field_len() } @@ -88,14 +88,14 @@ impl StructLikeInfo for StructVariantInfo { self.name() } - fn field_at(&self, index: usize) -> Option<&NamedField> { - self.field_at(index) - } - fn get_field(&self, name: &str) -> Option<&NamedField> { self.field(name) } + fn field_at(&self, index: usize) -> Option<&NamedField> { + self.field_at(index) + } + fn get_field_len(&self) -> usize { self.field_len() } @@ -219,7 +219,7 @@ impl Container for TupleVariantInfo { /// /// # Example /// -/// ```ignore +/// ```ignore (Can't import private struct from doctest) /// let expected = vec!["foo", "bar", "baz"]; /// assert_eq!("`foo`, `bar`, `baz`", format!("{}", ExpectedValues(expected))); /// ``` @@ -573,18 +573,18 @@ impl<'a, 'de> Visitor<'de> for StructVisitor<'a> { formatter.write_str("reflected struct value") } - fn visit_map(self, mut map: V) -> Result + fn visit_seq(self, mut seq: A) -> Result where - V: MapAccess<'de>, + A: SeqAccess<'de>, { - visit_struct(&mut map, self.struct_info, self.registration, self.registry) + visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) } - fn visit_seq(self, mut seq: A) -> Result + fn visit_map(self, mut map: V) -> Result where - A: SeqAccess<'de>, + V: MapAccess<'de>, { - visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) + visit_struct(&mut map, self.struct_info, self.registration, self.registry) } } @@ -831,29 +831,29 @@ impl<'de> DeserializeSeed<'de> for VariantDeserializer { formatter.write_str("expected either a variant index or variant name") } - fn visit_str(self, variant_name: &str) -> Result + fn visit_u32(self, variant_index: u32) -> Result where E: Error, { - self.0.variant(variant_name).ok_or_else(|| { - let names = self.0.iter().map(|variant| variant.name()); + self.0.variant_at(variant_index as usize).ok_or_else(|| { Error::custom(format_args!( - "unknown variant `{}`, expected one of {:?}", - variant_name, - ExpectedValues(names.collect()) + "no variant found at index `{}` on enum `{}`", + variant_index, + self.0.type_path() )) }) } - fn visit_u32(self, variant_index: u32) -> Result + fn visit_str(self, variant_name: &str) -> Result where E: Error, { - self.0.variant_at(variant_index as usize).ok_or_else(|| { + self.0.variant(variant_name).ok_or_else(|| { + let names = self.0.iter().map(|variant| variant.name()); Error::custom(format_args!( - "no variant found at index `{}` on enum `{}`", - variant_index, - self.0.type_path() + "unknown variant `{}`, expected one of {:?}", + variant_name, + ExpectedValues(names.collect()) )) }) } @@ -876,18 +876,18 @@ impl<'a, 'de> Visitor<'de> for StructVariantVisitor<'a> { formatter.write_str("reflected struct variant value") } - fn visit_map(self, mut map: V) -> Result + fn visit_seq(self, mut seq: A) -> Result where - V: MapAccess<'de>, + A: SeqAccess<'de>, { - visit_struct(&mut map, self.struct_info, self.registration, self.registry) + visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) } - fn visit_seq(self, mut seq: A) -> Result + fn visit_map(self, mut map: V) -> Result where - A: SeqAccess<'de>, + V: MapAccess<'de>, { - visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) + visit_struct(&mut map, self.struct_info, self.registration, self.registry) } } @@ -925,6 +925,15 @@ impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> { formatter.write_str(self.enum_info.type_path()) } + fn visit_none(self) -> Result + where + E: Error, + { + let mut option = DynamicEnum::default(); + option.set_variant("None", ()); + Ok(option) + } + fn visit_some(self, deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -951,15 +960,6 @@ impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> { ))), } } - - fn visit_none(self) -> Result - where - E: Error, - { - let mut option = DynamicEnum::default(); - option.set_variant("None", ()); - Ok(option) - } } fn visit_struct<'de, T, V>( diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 4f2f25bbd74be..0788ae045f847 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -426,9 +426,22 @@ impl Reflect for DynamicStruct { self } - #[inline] - fn clone_value(&self) -> Box { - Box::new(self.clone_dynamic()) + fn apply(&mut self, value: &dyn Reflect) { + if let ReflectRef::Struct(struct_value) = value.reflect_ref() { + for (i, value) in struct_value.iter_fields().enumerate() { + let name = struct_value.name_at(i).unwrap(); + if let Some(v) = self.field_mut(name) { + v.apply(value); + } + } + } else { + panic!("Attempted to apply non-struct type to struct type."); + } + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) } #[inline] @@ -446,22 +459,9 @@ impl Reflect for DynamicStruct { ReflectOwned::Struct(self) } - fn apply(&mut self, value: &dyn Reflect) { - if let ReflectRef::Struct(struct_value) = value.reflect_ref() { - for (i, value) in struct_value.iter_fields().enumerate() { - let name = struct_value.name_at(i).unwrap(); - if let Some(v) = self.field_mut(name) { - v.apply(value); - } - } - } else { - panic!("Attempted to apply non-struct type to struct type."); - } - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + #[inline] + fn clone_value(&self) -> Box { + Box::new(self.clone_dynamic()) } fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 747fb29e391ad..bbd973c2f28ef 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -335,9 +335,13 @@ impl Reflect for DynamicTuple { self } - #[inline] - fn clone_value(&self) -> Box { - Box::new(self.clone_dynamic()) + fn apply(&mut self, value: &dyn Reflect) { + tuple_apply(self, value); + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) } #[inline] @@ -355,13 +359,9 @@ impl Reflect for DynamicTuple { ReflectOwned::Tuple(self) } - fn apply(&mut self, value: &dyn Reflect) { - tuple_apply(self, value); - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + #[inline] + fn clone_value(&self) -> Box { + Box::new(self.clone_dynamic()) } fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index f0c453575baf5..8e3a13d97128a 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -329,9 +329,21 @@ impl Reflect for DynamicTupleStruct { self } - #[inline] - fn clone_value(&self) -> Box { - Box::new(self.clone_dynamic()) + fn apply(&mut self, value: &dyn Reflect) { + if let ReflectRef::TupleStruct(tuple_struct) = value.reflect_ref() { + for (i, value) in tuple_struct.iter_fields().enumerate() { + if let Some(v) = self.field_mut(i) { + v.apply(value); + } + } + } else { + panic!("Attempted to apply non-TupleStruct type to TupleStruct type."); + } + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) } #[inline] @@ -349,21 +361,9 @@ impl Reflect for DynamicTupleStruct { ReflectOwned::TupleStruct(self) } - fn apply(&mut self, value: &dyn Reflect) { - if let ReflectRef::TupleStruct(tuple_struct) = value.reflect_ref() { - for (i, value) in tuple_struct.iter_fields().enumerate() { - if let Some(v) = self.field_mut(i) { - v.apply(value); - } - } - } else { - panic!("Attempted to apply non-TupleStruct type to TupleStruct type."); - } - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + #[inline] + fn clone_value(&self) -> Box { + Box::new(self.clone_dynamic()) } fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { diff --git a/crates/bevy_reflect/src/type_path.rs b/crates/bevy_reflect/src/type_path.rs index 99f9b81e6e468..d48e620d68cd5 100644 --- a/crates/bevy_reflect/src/type_path.rs +++ b/crates/bevy_reflect/src/type_path.rs @@ -36,7 +36,7 @@ use std::fmt; /// /// # Example /// -/// ```rust +/// ``` /// use bevy_reflect::TypePath; /// /// // This type path will not change with compiler versions or recompiles, diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index ad32f5b7a2e70..6de79e5a4ffb4 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -101,8 +101,8 @@ impl TypeRegistry { } /// Registers the type `T`, adding reflect data as specified in the [`Reflect`] derive: - /// ```rust,ignore - /// #[derive(Reflect)] + /// ```ignore (Neither bevy_ecs nor serde "derive" are available.) + /// #[derive(Component, serde::Serialize, serde::Deserialize, Reflect)] /// #[reflect(Component, Serialize, Deserialize)] // will register ReflectComponent, ReflectSerialize, ReflectDeserialize /// ``` pub fn register(&mut self) @@ -143,7 +143,7 @@ impl TypeRegistry { /// this method can be used to insert additional type data. /// /// # Example - /// ```rust + /// ``` /// use bevy_reflect::{TypeRegistry, ReflectSerialize, ReflectDeserialize}; /// /// let mut type_registry = TypeRegistry::default(); @@ -499,7 +499,7 @@ impl Deserialize<'a> + Reflect> FromType for ReflectDeserialize { /// from a pointer. /// /// # Example -/// ```rust +/// ``` /// use bevy_reflect::{TypeRegistry, Reflect, ReflectFromPtr}; /// use bevy_ptr::Ptr; /// use std::ptr::NonNull; diff --git a/crates/bevy_reflect/src/type_uuid_impl.rs b/crates/bevy_reflect/src/type_uuid_impl.rs index 3860117a6c17a..9e614171ef57f 100644 --- a/crates/bevy_reflect/src/type_uuid_impl.rs +++ b/crates/bevy_reflect/src/type_uuid_impl.rs @@ -2,8 +2,10 @@ use crate::TypeUuid; use crate::{self as bevy_reflect, __macro_exports::generate_composite_uuid}; use bevy_reflect_derive::impl_type_uuid; use bevy_utils::{all_tuples, Duration, HashMap, HashSet, Instant, Uuid}; + #[cfg(feature = "smallvec")] -use smallvec::SmallVec; +use bevy_utils::{smallvec, smallvec::SmallVec}; + #[cfg(any(unix, windows))] use std::ffi::OsString; use std::{ diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index caf5eb0d9de4c..f5724f30d6c3b 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -20,7 +20,7 @@ dds = ["ddsfile"] pnm = ["image/pnm"] bevy_ci_testing = ["bevy_app/bevy_ci_testing"] -shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out"] +shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] # For ktx2 supercompression @@ -67,16 +67,17 @@ wgpu = { version = "0.18", features = [ "fragile-send-sync-non-atomic-wasm", ] } naga = { version = "0.14.2", features = ["wgsl-in"] } -naga_oil = "0.11" +naga_oil = { version = "0.11", default-features = false, features = [ + "test_shader", +] } serde = { version = "1", features = ["derive"] } bitflags = "2.3" bytemuck = { version = "1.5", features = ["derive"] } -smallvec = { version = "1.6", features = ["union", "const_generics"] } downcast-rs = "1.2.0" thread_local = "1.1" thiserror = "1.0" futures-lite = "2.0.1" -hexasphere = "9.0" +hexasphere = "10.0" ddsfile = { version = "0.5.0", optional = true } ktx2 = { version = "0.3.0", optional = true } # For ktx2 supercompression @@ -84,12 +85,12 @@ flate2 = { version = "1.0.22", optional = true } ruzstd = { version = "0.4.0", optional = true } # For transcoding of UASTC/ETC1S universal formats, and for .basis file support basis-universal = { version = "0.3.0", optional = true } -encase = { version = "0.6.1", features = ["glam"] } +encase = { version = "0.7", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = [ "profile-with-tracing", ], optional = true } -async-channel = "1.8" +async-channel = "2.1.0" [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3" diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 9e750fd57ed77..b1c7a12e0b0bc 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -11,6 +11,7 @@ use syn::{ const UNIFORM_ATTRIBUTE_NAME: Symbol = Symbol("uniform"); const TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("texture"); +const STORAGE_TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("storage_texture"); const SAMPLER_ATTRIBUTE_NAME: Symbol = Symbol("sampler"); const STORAGE_ATTRIBUTE_NAME: Symbol = Symbol("storage"); const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data"); @@ -19,6 +20,7 @@ const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data"); enum BindingType { Uniform, Texture, + StorageTexture, Sampler, Storage, } @@ -133,6 +135,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { BindingType::Uniform } else if attr_ident == TEXTURE_ATTRIBUTE_NAME { BindingType::Texture + } else if attr_ident == STORAGE_TEXTURE_ATTRIBUTE_NAME { + BindingType::StorageTexture } else if attr_ident == SAMPLER_ATTRIBUTE_NAME { BindingType::Sampler } else if attr_ident == STORAGE_ATTRIBUTE_NAME { @@ -255,6 +259,45 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { } }); } + BindingType::StorageTexture => { + let StorageTextureAttrs { + dimension, + image_format, + access, + visibility, + } = get_storage_texture_binding_attr(nested_meta_items)?; + + let visibility = + visibility.hygienic_quote("e! { #render_path::render_resource }); + + let fallback_image = get_fallback_image(&render_path, dimension); + + binding_impls.push(quote! { + ( #binding_index, + #render_path::render_resource::OwnedBindingResource::TextureView({ + let handle: Option<&#asset_path::Handle<#render_path::texture::Image>> = (&self.#field_name).into(); + if let Some(handle) = handle { + images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?.texture_view.clone() + } else { + #fallback_image.texture_view.clone() + } + }) + ) + }); + + binding_layouts.push(quote! { + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::StorageTexture { + access: #render_path::render_resource::StorageTextureAccess::#access, + format: #render_path::render_resource::TextureFormat::#image_format, + view_dimension: #render_path::render_resource::#dimension, + }, + count: None, + } + }); + } BindingType::Texture => { let TextureAttrs { dimension, @@ -585,6 +628,10 @@ impl ShaderStageVisibility { fn vertex_fragment() -> Self { Self::Flags(VisibilityFlags::vertex_fragment()) } + + fn compute() -> Self { + Self::Flags(VisibilityFlags::compute()) + } } impl VisibilityFlags { @@ -595,6 +642,13 @@ impl VisibilityFlags { ..Default::default() } } + + fn compute() -> Self { + Self { + compute: true, + ..Default::default() + } + } } impl ShaderStageVisibility { @@ -741,7 +795,72 @@ impl Default for TextureAttrs { } } +struct StorageTextureAttrs { + dimension: BindingTextureDimension, + // Parsing of the image_format parameter is deferred to the type checker, + // which will error if the format is not member of the TextureFormat enum. + image_format: proc_macro2::TokenStream, + // Parsing of the access parameter is deferred to the type checker, + // which will error if the access is not member of the StorageTextureAccess enum. + access: proc_macro2::TokenStream, + visibility: ShaderStageVisibility, +} + +impl Default for StorageTextureAttrs { + fn default() -> Self { + Self { + dimension: Default::default(), + image_format: quote! { Rgba8Unorm }, + access: quote! { ReadWrite }, + visibility: ShaderStageVisibility::compute(), + } + } +} + +fn get_storage_texture_binding_attr(metas: Vec) -> Result { + let mut storage_texture_attrs = StorageTextureAttrs::default(); + + for meta in metas { + use syn::Meta::{List, NameValue}; + match meta { + // Parse #[storage_texture(0, dimension = "...")]. + NameValue(m) if m.path == DIMENSION => { + let value = get_lit_str(DIMENSION, &m.value)?; + storage_texture_attrs.dimension = get_texture_dimension_value(value)?; + } + // Parse #[storage_texture(0, format = ...))]. + NameValue(m) if m.path == IMAGE_FORMAT => { + storage_texture_attrs.image_format = m.value.into_token_stream(); + } + // Parse #[storage_texture(0, access = ...))]. + NameValue(m) if m.path == ACCESS => { + storage_texture_attrs.access = m.value.into_token_stream(); + } + // Parse #[storage_texture(0, visibility(...))]. + List(m) if m.path == VISIBILITY => { + storage_texture_attrs.visibility = get_visibility_flag_value(&m)?; + } + NameValue(m) => { + return Err(Error::new_spanned( + m.path, + "Not a valid name. Available attributes: `dimension`, `image_format`, `access`.", + )); + } + _ => { + return Err(Error::new_spanned( + meta, + "Not a name value pair: `foo = \"...\"`", + )); + } + } + } + + Ok(storage_texture_attrs) +} + const DIMENSION: Symbol = Symbol("dimension"); +const IMAGE_FORMAT: Symbol = Symbol("image_format"); +const ACCESS: Symbol = Symbol("access"); const SAMPLE_TYPE: Symbol = Symbol("sample_type"); const FILTERABLE: Symbol = Symbol("filterable"); const MULTISAMPLED: Symbol = Symbol("multisampled"); diff --git a/crates/bevy_render/macros/src/extract_component.rs b/crates/bevy_render/macros/src/extract_component.rs index 8e0f5f55860f4..80d1aa57ca5b5 100644 --- a/crates/bevy_render/macros/src/extract_component.rs +++ b/crates/bevy_render/macros/src/extract_component.rs @@ -38,12 +38,12 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { TokenStream::from(quote! { impl #impl_generics #bevy_render_path::extract_component::ExtractComponent for #struct_name #type_generics #where_clause { - type Data = &'static Self; + type QueryData = &'static Self; - type Filter = #filter; + type QueryFilter = #filter; type Out = Self; - fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, Self::Data>) -> 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 89eec6b220c9a..97126ba830bf4 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -51,7 +51,7 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { #[proc_macro_derive( AsBindGroup, - attributes(uniform, texture, sampler, bind_group_data, storage) + attributes(uniform, storage_texture, texture, sampler, bind_group_data, storage) )] pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/crates/bevy_render/src/batching/mod.rs b/crates/bevy_render/src/batching/mod.rs index 5f7d5f69d2d27..859d56e98deb5 100644 --- a/crates/bevy_render/src/batching/mod.rs +++ b/crates/bevy_render/src/batching/mod.rs @@ -1,7 +1,7 @@ use bevy_ecs::{ component::Component, + entity::Entity, prelude::Res, - query::{QueryFilter, QueryItem, ReadOnlyQueryData}, system::{Query, ResMut, StaticSystemParam, SystemParam, SystemParamItem}, }; use bevy_utils::nonmax::NonMaxU32; @@ -57,8 +57,6 @@ impl BatchMeta { /// items. pub trait GetBatchData { type Param: SystemParam + 'static; - type Data: ReadOnlyQueryData; - type Filter: QueryFilter; /// Data used for comparison between phase items. If the pipeline id, draw /// function id, per-instance data buffer dynamic offset and this data /// matches, the draws can be batched. @@ -72,8 +70,8 @@ pub trait GetBatchData { /// for the `CompareData`. fn get_batch_data( param: &SystemParamItem, - query_item: &QueryItem, - ) -> (Self::BufferData, Option); + query_item: Entity, + ) -> Option<(Self::BufferData, Option)>; } /// Batch the items in a render phase. This means comparing metadata needed to draw each phase item @@ -81,16 +79,13 @@ pub trait GetBatchData { pub fn batch_and_prepare_render_phase( gpu_array_buffer: ResMut>, mut views: Query<&mut RenderPhase>, - query: Query, param: StaticSystemParam, ) { let gpu_array_buffer = gpu_array_buffer.into_inner(); let system_param_item = param.into_inner(); let mut process_item = |item: &mut I| { - let batch_query_item = query.get(item.entity()).ok()?; - - let (buffer_data, compare_data) = F::get_batch_data(&system_param_item, &batch_query_item); + let (buffer_data, compare_data) = F::get_batch_data(&system_param_item, item.entity())?; let buffer_index = gpu_array_buffer.push(buffer_data); let index = buffer_index.index.get(); diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index ecffba2d8294b..540f0fc3e30f5 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -24,15 +24,17 @@ use bevy_math::{ primitives::Direction3d, vec2, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3, }; use bevy_reflect::prelude::*; +use bevy_render_macros::ExtractComponent; use bevy_transform::components::GlobalTransform; use bevy_utils::{HashMap, HashSet}; use bevy_window::{ NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, + WindowScaleFactorChanged, }; use std::{borrow::Cow, ops::Range}; -use wgpu::{BlendState, LoadOp, TextureFormat}; +use wgpu::{BlendState, LoadOp, TextureFormat, TextureUsages}; -use super::Projection; +use super::{ClearColorConfig, Projection}; /// Render viewport configuration for the [`Camera`] component. /// @@ -65,9 +67,12 @@ impl Default for Viewport { /// Information about the current [`RenderTarget`]. #[derive(Default, Debug, Clone)] pub struct RenderTargetInfo { - /// The physical size of this render target (ignores scale factor). + /// The physical size of this render target (in physical pixels, ignoring scale factor). pub physical_size: UVec2, /// The scale factor of this render target. + /// + /// When rendering to a window, typically it is a value greater or equal than 1.0, + /// representing the ratio between the size of the window in physical pixels and the logical size of the window. pub scale_factor: f32, } @@ -76,10 +81,77 @@ pub struct RenderTargetInfo { pub struct ComputedCameraValues { projection_matrix: Mat4, target_info: Option, - // position and size of the `Viewport` + // size of the `Viewport` old_viewport_size: Option, } +/// How much energy a `Camera3d` absorbs from incoming light. +/// +/// +#[derive(Component)] +pub struct ExposureSettings { + /// + pub ev100: f32, +} + +impl ExposureSettings { + pub const EV100_SUNLIGHT: f32 = 15.0; + pub const EV100_OVERCAST: f32 = 12.0; + pub const EV100_INDOOR: f32 = 7.0; + + pub fn from_physical_camera(physical_camera_parameters: PhysicalCameraParameters) -> Self { + Self { + ev100: physical_camera_parameters.ev100(), + } + } + + /// Converts EV100 values to exposure values. + /// + #[inline] + pub fn exposure(&self) -> f32 { + (-self.ev100).exp2() / 1.2 + } +} + +impl Default for ExposureSettings { + fn default() -> Self { + Self { + ev100: Self::EV100_INDOOR, + } + } +} + +/// Parameters based on physical camera characteristics for calculating +/// EV100 values for use with [`ExposureSettings`]. +#[derive(Clone, Copy)] +pub struct PhysicalCameraParameters { + /// + pub aperture_f_stops: f32, + /// + pub shutter_speed_s: f32, + /// + pub sensitivity_iso: f32, +} + +impl PhysicalCameraParameters { + /// Calculate the [EV100](https://en.wikipedia.org/wiki/Exposure_value). + pub fn ev100(&self) -> f32 { + (self.aperture_f_stops * self.aperture_f_stops * 100.0 + / (self.shutter_speed_s * self.sensitivity_iso)) + .log2() + } +} + +impl Default for PhysicalCameraParameters { + fn default() -> Self { + Self { + aperture_f_stops: 1.0, + shutter_speed_s: 1.0 / 125.0, + sensitivity_iso: 100.0, + } + } +} + /// The defining [`Component`] for camera entities, /// storing information about how and what to render through this camera. /// @@ -118,6 +190,8 @@ pub struct Camera { /// "write their results on top" of previous camera results, and include them as a part of their render results. This is enabled by default to ensure /// cameras with MSAA enabled layer their results in the same way as cameras without MSAA enabled by default. pub msaa_writeback: bool, + /// The clear color operation to perform on the render target. + pub clear_color: ClearColorConfig, } impl Default for Camera { @@ -131,6 +205,7 @@ impl Default for Camera { output_mode: Default::default(), hdr: false, msaa_writeback: true, + clear_color: Default::default(), } } } @@ -190,7 +265,8 @@ impl Camera { .or_else(|| self.logical_target_size()) } - /// The physical size of this camera's viewport. If the `viewport` field is set to [`Some`], this + /// The physical size of this camera's viewport (in physical pixels). + /// If the `viewport` field is set to [`Some`], this /// will be the size of that custom viewport. Otherwise it will default to the full physical size of /// the current [`RenderTarget`]. /// For logic that requires the full physical size of the [`RenderTarget`], prefer [`Camera::physical_target_size`]. @@ -213,7 +289,8 @@ impl Camera { .and_then(|t| self.to_logical(t.physical_size)) } - /// The full physical size of this camera's [`RenderTarget`], ignoring custom `viewport` configuration. + /// The full physical size of this camera's [`RenderTarget`] (in physical pixels), + /// ignoring custom `viewport` configuration. /// Note that if the `viewport` field is [`Some`], this will not represent the size of the rendered area. /// For logic that requires the size of the actually rendered area, prefer [`Camera::physical_viewport_size`]. #[inline] @@ -221,6 +298,11 @@ impl Camera { self.computed.target_info.as_ref().map(|t| t.physical_size) } + #[inline] + pub fn target_scaling_factor(&self) -> Option { + self.computed.target_info.as_ref().map(|t| t.scale_factor) + } + /// The projection matrix computed using this camera's [`CameraProjection`]. #[inline] pub fn projection_matrix(&self) -> Mat4 { @@ -425,6 +507,12 @@ pub enum RenderTarget { TextureView(ManualTextureViewHandle), } +impl From> for RenderTarget { + fn from(handle: Handle) -> Self { + Self::Image(handle) + } +} + /// Normalized version of the render target. /// /// Once we have this we shouldn't need to resolve it down anymore. @@ -456,6 +544,16 @@ impl RenderTarget { RenderTarget::TextureView(id) => Some(NormalizedRenderTarget::TextureView(*id)), } } + + /// Get a handle to the render target's image, + /// or `None` if the render target is another variant. + pub fn as_image(&self) -> Option<&Handle> { + if let Self::Image(handle) = self { + Some(handle) + } else { + None + } + } } impl NormalizedRenderTarget { @@ -551,9 +649,9 @@ impl NormalizedRenderTarget { /// System in charge of updating a [`Camera`] when its window or projection changes. /// -/// The system detects window creation and resize events to update the camera projection if -/// needed. It also queries any [`CameraProjection`] component associated with the same entity -/// as the [`Camera`] one, to automatically update the camera projection matrix. +/// The system detects window creation, resize, and scale factor change events to update the camera +/// projection if needed. It also queries any [`CameraProjection`] component associated with the same +/// entity as the [`Camera`] one, to automatically update the camera projection matrix. /// /// The system function is generic over the camera projection type, and only instances of /// [`OrthographicProjection`] and [`PerspectiveProjection`] are automatically added to @@ -571,6 +669,7 @@ impl NormalizedRenderTarget { pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, + mut window_scale_factor_changed_events: EventReader, mut image_asset_events: EventReader>, primary_window: Query>, windows: Query<(Entity, &Window)>, @@ -583,6 +682,11 @@ pub fn camera_system( let mut changed_window_ids = HashSet::new(); changed_window_ids.extend(window_created_events.read().map(|event| event.window)); changed_window_ids.extend(window_resized_events.read().map(|event| event.window)); + let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_events + .read() + .map(|event| event.window) + .collect(); + changed_window_ids.extend(scale_factor_changed_window_ids.clone()); let changed_image_handles: HashSet<&AssetId> = image_asset_events .read() @@ -593,7 +697,7 @@ pub fn camera_system( .collect(); for (mut camera, mut camera_projection) in &mut cameras { - let viewport_size = camera + let mut viewport_size = camera .viewport .as_ref() .map(|viewport| viewport.physical_size); @@ -604,11 +708,36 @@ pub fn camera_system( || camera_projection.is_changed() || camera.computed.old_viewport_size != viewport_size { - camera.computed.target_info = normalized_target.get_render_target_info( + let new_computed_target_info = normalized_target.get_render_target_info( &windows, &images, &manual_texture_views, ); + // Check for the scale factor changing, and resize the viewport if needed. + // This can happen when the window is moved between monitors with different DPIs. + // Without this, the viewport will take a smaller portion of the window moved to + // a higher DPI monitor. + if normalized_target.is_changed(&scale_factor_changed_window_ids, &HashSet::new()) { + if let (Some(new_scale_factor), Some(old_scale_factor)) = ( + new_computed_target_info + .as_ref() + .map(|info| info.scale_factor), + camera + .computed + .target_info + .as_ref() + .map(|info| info.scale_factor), + ) { + let resize_factor = new_scale_factor / old_scale_factor; + if let Some(ref mut viewport) = camera.viewport { + let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2(); + viewport.physical_position = resize(viewport.physical_position); + viewport.physical_size = resize(viewport.physical_size); + viewport_size = Some(viewport.physical_size); + } + } + } + camera.computed.target_info = new_computed_target_info; if let Some(size) = camera.logical_viewport_size() { camera_projection.update(size.x, size.y); camera.computed.projection_matrix = camera_projection.get_projection_matrix(); @@ -622,6 +751,19 @@ pub fn camera_system( } } +/// This component lets you control the [`TextureUsages`] field of the main texture generated for the camera +#[derive(Component, ExtractComponent, Clone, Copy)] +pub struct CameraMainTextureUsages(pub TextureUsages); +impl Default for CameraMainTextureUsages { + fn default() -> Self { + Self( + TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_SRC, + ) + } +} + #[derive(Component, Debug)] pub struct ExtractedCamera { pub target: Option, @@ -632,7 +774,9 @@ pub struct ExtractedCamera { pub order: isize, pub output_mode: CameraOutputMode, pub msaa_writeback: bool, + pub clear_color: ClearColorConfig, pub sorted_camera_index_for_target: usize, + pub exposure: f32, } pub fn extract_cameras( @@ -646,6 +790,7 @@ pub fn extract_cameras( &VisibleEntities, &Frustum, Option<&ColorGrading>, + Option<&ExposureSettings>, Option<&TemporalJitter>, Option<&RenderLayers>, Option<&Projection>, @@ -662,6 +807,7 @@ pub fn extract_cameras( visible_entities, frustum, color_grading, + exposure_settings, temporal_jitter, render_layers, projection, @@ -701,8 +847,12 @@ pub fn extract_cameras( order: camera.order, output_mode: camera.output_mode, msaa_writeback: camera.msaa_writeback, + clear_color: camera.clear_color.clone(), // this will be set in sort_cameras sorted_camera_index_for_target: 0, + exposure: exposure_settings + .map(|e| e.exposure()) + .unwrap_or_else(|| ExposureSettings::default().exposure()), }, ExtractedView { projection: camera.projection_matrix(), diff --git a/crates/bevy_core_pipeline/src/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs similarity index 95% rename from crates/bevy_core_pipeline/src/clear_color.rs rename to crates/bevy_render/src/camera/clear_color.rs index 94832ce5d1dec..952e22ad976fd 100644 --- a/crates/bevy_core_pipeline/src/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -1,7 +1,7 @@ +use crate::{color::Color, extract_resource::ExtractResource}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_render::{color::Color, extract_resource::ExtractResource}; use serde::{Deserialize, Serialize}; /// For a camera, specifies the color used to clear the viewport before rendering. diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 2a92ff159692c..214ced79d25fc 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -1,17 +1,19 @@ #[allow(clippy::module_inception)] mod camera; mod camera_driver_node; +mod clear_color; mod manual_texture_view; mod projection; pub use camera::*; pub use camera_driver_node::*; +pub use clear_color::*; pub use manual_texture_view::*; pub use projection::*; use crate::{ - extract_resource::ExtractResourcePlugin, render_graph::RenderGraph, ExtractSchedule, Render, - RenderApp, RenderSet, + extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, + render_graph::RenderGraph, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; use bevy_ecs::schedule::IntoSystemConfigs; @@ -27,12 +29,17 @@ impl Plugin for CameraPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::() .init_resource::() + .init_resource::() .add_plugins(( CameraProjectionPlugin::::default(), CameraProjectionPlugin::::default(), CameraProjectionPlugin::::default(), ExtractResourcePlugin::::default(), + ExtractResourcePlugin::::default(), + ExtractComponentPlugin::::default(), )); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 25b495b4f1dec..27f90559d6588 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,4 +1,5 @@ use std::marker::PhantomData; +use std::ops::{Div, DivAssign, Mul, MulAssign}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; @@ -192,7 +193,20 @@ impl Default for PerspectiveProjection { } } -#[derive(Debug, Clone, Reflect, Serialize, Deserialize)] +/// Scaling mode for [`OrthographicProjection`]. +/// +/// # Examples +/// +/// Configure the orthographic projection to two world units per window height: +/// +/// ``` +/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode}; +/// let projection = Projection::Orthographic(OrthographicProjection { +/// scaling_mode: ScalingMode::FixedVertical(2.0), +/// ..OrthographicProjection::default() +/// }); +/// ``` +#[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] pub enum ScalingMode { /// Manually specify the projection's size, ignoring window resizing. The image will stretch. @@ -215,6 +229,60 @@ pub enum ScalingMode { FixedHorizontal(f32), } +impl Mul for ScalingMode { + type Output = ScalingMode; + + /// Scale the `ScalingMode`. For example, multiplying by 2 makes the viewport twice as large. + fn mul(self, rhs: f32) -> ScalingMode { + match self { + ScalingMode::Fixed { width, height } => ScalingMode::Fixed { + width: width * rhs, + height: height * rhs, + }, + ScalingMode::WindowSize(pixels_per_world_unit) => { + ScalingMode::WindowSize(pixels_per_world_unit / rhs) + } + ScalingMode::AutoMin { + min_width, + min_height, + } => ScalingMode::AutoMin { + min_width: min_width * rhs, + min_height: min_height * rhs, + }, + ScalingMode::AutoMax { + max_width, + max_height, + } => ScalingMode::AutoMax { + max_width: max_width * rhs, + max_height: max_height * rhs, + }, + ScalingMode::FixedVertical(size) => ScalingMode::FixedVertical(size * rhs), + ScalingMode::FixedHorizontal(size) => ScalingMode::FixedHorizontal(size * rhs), + } + } +} + +impl MulAssign for ScalingMode { + fn mul_assign(&mut self, rhs: f32) { + *self = *self * rhs; + } +} + +impl Div for ScalingMode { + type Output = ScalingMode; + + /// Scale the `ScalingMode`. For example, dividing by 2 makes the viewport half as large. + fn div(self, rhs: f32) -> ScalingMode { + self * (1.0 / rhs) + } +} + +impl DivAssign for ScalingMode { + fn div_assign(&mut self, rhs: f32) { + *self = *self / rhs; + } +} + /// Project a 3D space onto a 2D surface using parallel lines, i.e., unlike [`PerspectiveProjection`], /// the size of objects remains the same regardless of their distance to the camera. /// @@ -223,6 +291,18 @@ pub enum ScalingMode { /// /// Note that the scale of the projection and the apparent size of objects are inversely proportional. /// As the size of the projection increases, the size of objects decreases. +/// +/// # Examples +/// +/// Configure the orthographic projection to one world unit per 100 window pixels: +/// +/// ``` +/// # use bevy_render::camera::{OrthographicProjection, Projection, ScalingMode}; +/// let projection = Projection::Orthographic(OrthographicProjection { +/// scaling_mode: ScalingMode::WindowSize(100.0), +/// ..OrthographicProjection::default() +/// }); +/// ``` #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct OrthographicProjection { @@ -251,15 +331,20 @@ pub struct OrthographicProjection { /// Defaults to `(0.5, 0.5)`, which makes scaling affect opposite sides equally, keeping the center /// point of the viewport centered. pub viewport_origin: Vec2, - /// How the projection will scale when the viewport is resized. + /// How the projection will scale to the viewport. /// /// Defaults to `ScalingMode::WindowSize(1.0)` pub scaling_mode: ScalingMode, - /// Scales the projection in world units. + /// Scales the projection. /// /// As scale increases, the apparent size of objects decreases, and vice versa. /// - /// Defaults to `1.0` + /// Note: scaling can be set by [`scaling_mode`](Self::scaling_mode) as well. + /// This parameter scales on top of that. + /// + /// This property is particularly useful in implementing zoom functionality. + /// + /// Defaults to `1.0`. pub scale: f32, /// The area that the projection covers relative to `viewport_origin`. /// diff --git a/crates/bevy_render/src/deterministic.rs b/crates/bevy_render/src/deterministic.rs new file mode 100644 index 0000000000000..ec89116a5e8e7 --- /dev/null +++ b/crates/bevy_render/src/deterministic.rs @@ -0,0 +1,14 @@ +use bevy_ecs::system::Resource; + +/// Configure deterministic rendering to fix flickering due to z-fighting. +#[derive(Resource, Default)] +pub struct DeterministicRenderingConfig { + /// Sort visible entities by id before rendering to avoid flickering. + /// + /// Render is parallel by default, and if there's z-fighting, it may cause flickering. + /// Default fix for the issue is to set `depth_bias` per material. + /// When it is not possible, entities sorting can be used. + /// + /// This option costs performance and disabled by default. + pub stable_sort_z_fighting: bool, +} diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 63ef625a170e3..98be77b2b9689 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -36,9 +36,9 @@ impl DynamicUniformIndex { /// in the [`ExtractSchedule`] step. pub trait ExtractComponent: Component { /// ECS [`ReadOnlyQueryData`] to fetch the components to extract. - type Data: ReadOnlyQueryData; + type QueryData: ReadOnlyQueryData; /// Filters the entities with additional constraints. - type Filter: QueryFilter; + type QueryFilter: QueryFilter; /// The output from extraction. /// @@ -58,7 +58,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::Data>) -> Option; + fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option; } /// This plugin prepares the components of the corresponding type for the GPU @@ -195,12 +195,12 @@ impl Plugin for ExtractComponentPlugin { } impl ExtractComponent for Handle { - type Data = Read>; - type Filter = (); + type QueryData = Read>; + type QueryFilter = (); type Out = Handle; #[inline] - fn extract_component(handle: QueryItem<'_, Self::Data>) -> Option { + fn extract_component(handle: QueryItem<'_, Self::QueryData>) -> Option { Some(handle.clone_weak()) } } @@ -209,7 +209,7 @@ impl ExtractComponent for Handle { fn extract_components( mut commands: Commands, mut previous_len: Local, - query: Extract>, + query: Extract>, ) { let mut values = Vec::with_capacity(*previous_len); for (entity, query_item) in &query { @@ -225,7 +225,7 @@ fn extract_components( fn extract_visible_components( mut commands: Commands, mut previous_len: Local, - query: Extract>, + query: Extract>, ) { let mut values = Vec::with_capacity(*previous_len); for (entity, view_visibility, query_item) in &query { diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index 0b088e4607d7e..96fcfea7cb84e 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -29,12 +29,12 @@ use crate::{prelude::ViewVisibility, Extract, ExtractSchedule, RenderApp}; /// higher-performance because it avoids the ECS overhead. pub trait ExtractInstance: Send + Sync + Sized + 'static { /// ECS [`ReadOnlyQueryData`] to fetch the components to extract. - type Data: ReadOnlyQueryData; + type QueryData: ReadOnlyQueryData; /// Filters the entities with additional constraints. - type Filter: QueryFilter; + type QueryFilter: QueryFilter; /// Defines how the component is transferred into the "render world". - fn extract(item: QueryItem<'_, Self::Data>) -> Option; + fn extract(item: QueryItem<'_, Self::QueryData>) -> Option; } /// This plugin extracts one or more components into the "render world" as @@ -107,7 +107,7 @@ where fn extract_all( mut extracted_instances: ResMut>, - query: Extract>, + query: Extract>, ) where EI: ExtractInstance, { @@ -121,7 +121,7 @@ fn extract_all( fn extract_visible( mut extracted_instances: ResMut>, - query: Extract>, + query: Extract>, ) where EI: ExtractInstance, { @@ -139,10 +139,10 @@ impl ExtractInstance for AssetId where A: Asset, { - type Data = Read>; - type Filter = (); + type QueryData = Read>; + type QueryFilter = (); - fn extract(item: QueryItem<'_, Self::Data>) -> Option { + fn extract(item: QueryItem<'_, Self::QueryData>) -> Option { Some(item.id()) } } diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index e6b1ea1802fd3..02c925d4eb3f2 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -27,7 +27,7 @@ use std::ops::{Deref, DerefMut}; /// /// ## Examples /// -/// ```rust +/// ``` /// use bevy_ecs::prelude::*; /// use bevy_render::Extract; /// # #[derive(Component)] diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index a8243422d607d..5014b948537bc 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -6,6 +6,7 @@ extern crate core; pub mod batching; pub mod camera; pub mod color; +pub mod deterministic; pub mod extract_component; pub mod extract_instances; mod extract_param; @@ -13,6 +14,7 @@ pub mod extract_resource; pub mod globals; pub mod gpu_component_array_buffer; pub mod mesh; +#[cfg(not(target_arch = "wasm32"))] pub mod pipelined_rendering; pub mod primitives; pub mod render_asset; @@ -27,7 +29,10 @@ pub mod view; pub mod prelude { #[doc(hidden)] pub use crate::{ - camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection}, + camera::{ + Camera, ClearColor, ClearColorConfig, OrthographicProjection, PerspectiveProjection, + Projection, + }, color::Color, mesh::{morph::MorphWeights, shape, Mesh}, render_resource::Shader, @@ -45,6 +50,7 @@ use bevy_window::{PrimaryWindow, RawHandleWrapper}; use globals::GlobalsPlugin; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use crate::deterministic::DeterministicRenderingConfig; use crate::{ camera::CameraPlugin, mesh::{morph::MorphPlugin, Mesh, MeshPlugin}, @@ -64,6 +70,13 @@ use std::{ }; /// Contains the default Bevy rendering backend based on wgpu. +/// +/// Rendering is done in a [`SubApp`], which exchanges data with the main app +/// between main schedule iterations. +/// +/// Rendering can be executed between iterations of the main schedule, +/// or it can be executed in parallel with main schedule when +/// [`PipelinedRenderingPlugin`](pipelined_rendering::PipelinedRenderingPlugin) is enabled. #[derive(Default)] pub struct RenderPlugin { pub render_creation: RenderCreation, @@ -206,6 +219,8 @@ pub const MATHS_SHADER_HANDLE: Handle = Handle::weak_from_u128(106653563 impl Plugin for RenderPlugin { /// Initializes the renderer, sets up the [`RenderSet`] and creates the rendering sub-app. fn build(&self, app: &mut App) { + app.init_resource::(); + app.init_asset::() .init_asset_loader::(); diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 12713ce23b3d8..270fd86cb1705 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -5,7 +5,7 @@ pub use wgpu::PrimitiveTopology; use crate::{ prelude::Image, primitives::Aabb, - render_asset::{PrepareAssetError, RenderAsset, RenderAssets}, + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy, RenderAssets}, render_resource::{Buffer, TextureView, VertexBufferLayout}, renderer::RenderDevice, }; @@ -48,9 +48,10 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// ``` /// # use bevy_render::mesh::{Mesh, Indices}; /// # use bevy_render::render_resource::PrimitiveTopology; +/// # use bevy_render::render_asset::RenderAssetPersistencePolicy; /// fn create_simple_parallelogram() -> Mesh { /// // Create a new mesh using a triangle list topology, where each set of 3 vertices composes a triangle. -/// Mesh::new(PrimitiveTopology::TriangleList) +/// Mesh::new(PrimitiveTopology::TriangleList, RenderAssetPersistencePolicy::Unload) /// // Add 4 vertices, each with its own position attribute (coordinate in /// // 3D space), for each of the corners of the parallelogram. /// .with_inserted_attribute( @@ -108,8 +109,6 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// - Vertex winding order: by default, `StandardMaterial.cull_mode` is [`Some(Face::Back)`](crate::render_resource::Face), /// which means that Bevy would *only* render the "front" of each triangle, which /// is the side of the triangle from where the vertices appear in a *counter-clockwise* order. -/// -// TODO: allow values to be unloaded after been submitting to the GPU to conserve memory #[derive(Asset, Debug, Clone, Reflect)] pub struct Mesh { #[reflect(ignore)] @@ -123,6 +122,7 @@ pub struct Mesh { indices: Option, morph_targets: Option>, morph_target_names: Option>, + pub cpu_persistent_access: RenderAssetPersistencePolicy, } impl Mesh { @@ -139,12 +139,15 @@ impl Mesh { /// Texture coordinates for the vertex. Use in conjunction with [`Mesh::insert_attribute`] /// or [`Mesh::with_inserted_attribute`]. /// - /// Values are generally between 0. and 1., with `StandardMaterial` and `ColorMaterial` - /// `[0.,0.]` is the top left of the texture, and [1.,1.] the bottom-right. - /// You usually want to only use values in that range, values outside will be - /// clamped per pixel not for the vertex, "stretching" the borders of the texture. + /// Generally `[0.,0.]` is mapped to the top left of the texture, and `[1.,1.]` to the bottom-right. + /// + /// By default values outside will be clamped per pixel not for the vertex, + /// "stretching" the borders of the texture. /// This behavior can be useful in some cases, usually when the borders have only /// one color, for example a logo, and you want to "extend" those borders. + /// + /// For different mapping outside of `0..=1` range, + /// see [`ImageAddressMode`](crate::texture::ImageAddressMode). pub const ATTRIBUTE_UV_0: MeshVertexAttribute = MeshVertexAttribute::new("Vertex_Uv", 2, VertexFormat::Float32x2); @@ -180,13 +183,17 @@ impl Mesh { /// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the /// renderer knows how to treat the vertex data. Most of the time this will be /// [`PrimitiveTopology::TriangleList`]. - pub fn new(primitive_topology: PrimitiveTopology) -> Self { + pub fn new( + primitive_topology: PrimitiveTopology, + cpu_persistent_access: RenderAssetPersistencePolicy, + ) -> Self { Mesh { primitive_topology, attributes: Default::default(), indices: None, morph_targets: None, morph_target_names: None, + cpu_persistent_access, } } @@ -566,6 +573,9 @@ impl Mesh { } /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space + /// + /// Returns `None` if `self` doesn't have [`Mesh::ATTRIBUTE_POSITION`] of + /// type [`VertexAttributeValues::Float32x3`], or if `self` doesn't have any vertices. pub fn compute_aabb(&self) -> Option { let Some(VertexAttributeValues::Float32x3(values)) = self.attribute(Mesh::ATTRIBUTE_POSITION) @@ -1051,50 +1061,48 @@ pub enum GpuBufferInfo { } impl RenderAsset for Mesh { - type ExtractedAsset = Mesh; type PreparedAsset = GpuMesh; type Param = (SRes, SRes>); - /// Clones the mesh. - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() + fn persistence_policy(&self) -> RenderAssetPersistencePolicy { + self.cpu_persistent_access } /// Converts the extracted mesh a into [`GpuMesh`]. fn prepare_asset( - mesh: Self::ExtractedAsset, + self, (render_device, images): &mut SystemParamItem, - ) -> Result> { - let vertex_buffer_data = mesh.get_vertex_buffer_data(); + ) -> Result> { + let vertex_buffer_data = self.get_vertex_buffer_data(); let vertex_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, label: Some("Mesh Vertex Buffer"), contents: &vertex_buffer_data, }); - let buffer_info = if let Some(data) = mesh.get_index_buffer_bytes() { + let buffer_info = if let Some(data) = self.get_index_buffer_bytes() { GpuBufferInfo::Indexed { buffer: render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::INDEX, contents: data, label: Some("Mesh Index Buffer"), }), - count: mesh.indices().unwrap().len() as u32, - index_format: mesh.indices().unwrap().into(), + count: self.indices().unwrap().len() as u32, + index_format: self.indices().unwrap().into(), } } else { GpuBufferInfo::NonIndexed }; - let mesh_vertex_buffer_layout = mesh.get_mesh_vertex_buffer_layout(); + let mesh_vertex_buffer_layout = self.get_mesh_vertex_buffer_layout(); Ok(GpuMesh { vertex_buffer, - vertex_count: mesh.count_vertices() as u32, + vertex_count: self.count_vertices() as u32, buffer_info, - primitive_topology: mesh.primitive_topology(), + primitive_topology: self.primitive_topology(), layout: mesh_vertex_buffer_layout, - morph_targets: mesh + morph_targets: self .morph_targets .and_then(|mt| images.get(&mt).map(|i| i.texture_view.clone())), }) @@ -1225,12 +1233,16 @@ fn generate_tangents_for_mesh(mesh: &Mesh) -> Result, GenerateTang #[cfg(test)] mod tests { use super::Mesh; + use crate::render_asset::RenderAssetPersistencePolicy; use wgpu::PrimitiveTopology; #[test] #[should_panic] fn panic_invalid_format() { - let _mesh = Mesh::new(PrimitiveTopology::TriangleList) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0, 0.0]]); + let _mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Unload, + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0, 0.0]]); } } diff --git a/crates/bevy_render/src/mesh/morph.rs b/crates/bevy_render/src/mesh/morph.rs index cb523113be231..bde10bd3cad69 100644 --- a/crates/bevy_render/src/mesh/morph.rs +++ b/crates/bevy_render/src/mesh/morph.rs @@ -1,5 +1,6 @@ use crate::{ mesh::Mesh, + render_asset::RenderAssetPersistencePolicy, render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, }; @@ -67,6 +68,7 @@ impl MorphTargetImage { pub fn new( targets: impl ExactSizeIterator>, vertex_count: usize, + cpu_persistent_access: RenderAssetPersistencePolicy, ) -> Result { let max = MAX_TEXTURE_WIDTH; let target_count = targets.len(); @@ -101,7 +103,13 @@ impl MorphTargetImage { height, depth_or_array_layers: target_count as u32, }; - let image = Image::new(extents, TextureDimension::D3, data, TextureFormat::R32Float); + let image = Image::new( + extents, + TextureDimension::D3, + data, + TextureFormat::R32Float, + cpu_persistent_access, + ); Ok(MorphTargetImage(image)) } } @@ -114,7 +122,7 @@ impl MorphTargetImage { /// This exists because Bevy's [`Mesh`] corresponds to a _single_ surface / material, whereas morph targets /// as defined in the GLTF spec exist on "multi-primitive meshes" (where each primitive is its own surface with its own material). /// Therefore in Bevy [`MorphWeights`] an a parent entity are the "canonical weights" from a GLTF perspective, which then -/// synchronized to child [`Handle`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective). +/// synchronized to child [`Handle`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective). /// /// Add this to the parent of one or more [`Entities`](`Entity`) with a [`Handle`] with a [`MeshMorphWeights`]. /// diff --git a/crates/bevy_render/src/mesh/shape/capsule.rs b/crates/bevy_render/src/mesh/shape/capsule.rs index d438ed28bc3ee..865997a9b624f 100644 --- a/crates/bevy_render/src/mesh/shape/capsule.rs +++ b/crates/bevy_render/src/mesh/shape/capsule.rs @@ -1,4 +1,7 @@ -use crate::mesh::{Indices, Mesh}; +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; use bevy_math::{Vec2, Vec3}; use wgpu::PrimitiveTopology; @@ -364,10 +367,13 @@ impl From for Mesh { assert_eq!(vs.len(), vert_len); assert_eq!(tris.len(), fs_len); - Mesh::new(PrimitiveTopology::TriangleList) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts) - .with_indices(Some(Indices::U32(tris))) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vs) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vns) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, vts) + .with_indices(Some(Indices::U32(tris))) } } diff --git a/crates/bevy_render/src/mesh/shape/cylinder.rs b/crates/bevy_render/src/mesh/shape/cylinder.rs index a4d517ac73952..be08ea22ede91 100644 --- a/crates/bevy_render/src/mesh/shape/cylinder.rs +++ b/crates/bevy_render/src/mesh/shape/cylinder.rs @@ -1,4 +1,7 @@ -use crate::mesh::{Indices, Mesh}; +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; use wgpu::PrimitiveTopology; /// A cylinder which stands on the XZ plane @@ -118,10 +121,13 @@ impl From for Mesh { build_cap(true); build_cap(false); - Mesh::new(PrimitiveTopology::TriangleList) - .with_indices(Some(Indices::U32(indices))) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(Indices::U32(indices))) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) } } diff --git a/crates/bevy_render/src/mesh/shape/icosphere.rs b/crates/bevy_render/src/mesh/shape/icosphere.rs index 457ea0f82661d..af8158d5bd19a 100644 --- a/crates/bevy_render/src/mesh/shape/icosphere.rs +++ b/crates/bevy_render/src/mesh/shape/icosphere.rs @@ -1,4 +1,7 @@ -use crate::mesh::{Indices, Mesh}; +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; use hexasphere::shapes::IcoSphere; use thiserror::Error; use wgpu::PrimitiveTopology; @@ -103,10 +106,13 @@ impl TryFrom for Mesh { let indices = Indices::U32(indices); - Ok(Mesh::new(PrimitiveTopology::TriangleList) - .with_indices(Some(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)) + Ok(Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, points) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs)) } } diff --git a/crates/bevy_render/src/mesh/shape/mod.rs b/crates/bevy_render/src/mesh/shape/mod.rs index c9f6b9e1492bf..f7c7ce730959a 100644 --- a/crates/bevy_render/src/mesh/shape/mod.rs +++ b/crates/bevy_render/src/mesh/shape/mod.rs @@ -1,3 +1,5 @@ +use crate::render_asset::RenderAssetPersistencePolicy; + use super::{Indices, Mesh}; use bevy_math::*; @@ -120,11 +122,14 @@ impl From for Mesh { 20, 21, 22, 22, 23, 20, // bottom ]); - Mesh::new(PrimitiveTopology::TriangleList) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_indices(Some(indices)) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_indices(Some(indices)) } } @@ -172,11 +177,14 @@ impl From for Mesh { let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect(); let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect(); - Mesh::new(PrimitiveTopology::TriangleList) - .with_indices(Some(indices)) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(indices)) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) } } @@ -253,11 +261,14 @@ impl From for Mesh { } } - Mesh::new(PrimitiveTopology::TriangleList) - .with_indices(Some(Indices::U32(indices))) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(Indices::U32(indices))) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) } } diff --git a/crates/bevy_render/src/mesh/shape/regular_polygon.rs b/crates/bevy_render/src/mesh/shape/regular_polygon.rs index 879c59fabd11e..481dc5c8ce5c6 100644 --- a/crates/bevy_render/src/mesh/shape/regular_polygon.rs +++ b/crates/bevy_render/src/mesh/shape/regular_polygon.rs @@ -1,4 +1,7 @@ -use crate::mesh::{Indices, Mesh}; +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; use wgpu::PrimitiveTopology; /// A regular polygon in the `XY` plane @@ -55,11 +58,14 @@ impl From for Mesh { indices.extend_from_slice(&[0, i + 1, i]); } - Mesh::new(PrimitiveTopology::TriangleList) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) - .with_indices(Some(Indices::U32(indices))) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_indices(Some(Indices::U32(indices))) } } diff --git a/crates/bevy_render/src/mesh/shape/torus.rs b/crates/bevy_render/src/mesh/shape/torus.rs index 5254fcceebc01..19a58a259f68f 100644 --- a/crates/bevy_render/src/mesh/shape/torus.rs +++ b/crates/bevy_render/src/mesh/shape/torus.rs @@ -1,4 +1,7 @@ -use crate::mesh::{Indices, Mesh}; +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; use bevy_math::Vec3; use wgpu::PrimitiveTopology; @@ -84,10 +87,13 @@ impl From for Mesh { } } - Mesh::new(PrimitiveTopology::TriangleList) - .with_indices(Some(Indices::U32(indices))) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(Indices::U32(indices))) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) } } diff --git a/crates/bevy_render/src/mesh/shape/uvsphere.rs b/crates/bevy_render/src/mesh/shape/uvsphere.rs index b6b89ebc40157..c596e1823a359 100644 --- a/crates/bevy_render/src/mesh/shape/uvsphere.rs +++ b/crates/bevy_render/src/mesh/shape/uvsphere.rs @@ -1,6 +1,9 @@ use wgpu::PrimitiveTopology; -use crate::mesh::{Indices, Mesh}; +use crate::{ + mesh::{Indices, Mesh}, + render_asset::RenderAssetPersistencePolicy, +}; use std::f32::consts::PI; /// A sphere made of sectors and stacks. @@ -80,10 +83,13 @@ impl From for Mesh { } } - Mesh::new(PrimitiveTopology::TriangleList) - .with_indices(Some(Indices::U32(indices))) - .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices) - .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetPersistencePolicy::Keep, + ) + .with_indices(Some(Indices::U32(indices))) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) } } diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 8b74959dc6c3f..0e4dfd7dd961b 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -3,7 +3,7 @@ use std::borrow::Borrow; use bevy_ecs::{component::Component, prelude::Entity, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; use bevy_reflect::Reflect; -use bevy_utils::HashMap; +use bevy_utils::EntityHashMap; /// An axis-aligned bounding box, defined by: /// - a center, @@ -323,7 +323,7 @@ impl CubemapFrusta { #[reflect(Component)] pub struct CascadesFrusta { #[reflect(ignore)] - pub frusta: HashMap>, + pub frusta: EntityHashMap>, } #[cfg(test)] diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index e97874544a630..2ea2caaf5e779 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -1,12 +1,14 @@ -use crate::{Extract, ExtractSchedule, Render, RenderApp, RenderSet}; +use crate::{ExtractSchedule, MainWorld, Render, RenderApp, RenderSet}; use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; use bevy_ecs::{ - prelude::*, + prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource}, schedule::SystemConfigs, - system::{StaticSystemParam, SystemParam, SystemParamItem}, + system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState}, }; +use bevy_reflect::Reflect; use bevy_utils::{thiserror::Error, HashMap, HashSet}; +use serde::{Deserialize, Serialize}; use std::marker::PhantomData; #[derive(Debug, Error)] @@ -19,27 +21,43 @@ pub enum PrepareAssetError { /// /// In the [`ExtractSchedule`] step the asset is transferred /// from the "main world" into the "render world". -/// Therefore it is converted into a [`RenderAsset::ExtractedAsset`], which may be the same type -/// as the render asset itself. /// /// After that in the [`RenderSet::PrepareAssets`] step the extracted asset /// is transformed into its GPU-representation of type [`RenderAsset::PreparedAsset`]. -pub trait RenderAsset: Asset { - /// The representation of the asset in the "render world". - type ExtractedAsset: Send + Sync + 'static; +pub trait RenderAsset: Asset + Clone { /// The GPU-representation of the asset. type PreparedAsset: Send + Sync + 'static; + /// Specifies all ECS data required by [`RenderAsset::prepare_asset`]. + /// /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. type Param: SystemParam; - /// Converts the asset into a [`RenderAsset::ExtractedAsset`]. - fn extract_asset(&self) -> Self::ExtractedAsset; - /// Prepares the `extracted asset` for the GPU by transforming it into - /// a [`RenderAsset::PreparedAsset`]. Therefore ECS data may be accessed via the `param`. + + /// Whether or not to unload the asset after extracting it to the render world. + fn persistence_policy(&self) -> RenderAssetPersistencePolicy; + + /// Prepares the asset for the GPU by transforming it into a [`RenderAsset::PreparedAsset`]. + /// + /// ECS data may be accessed via `param`. fn prepare_asset( - extracted_asset: Self::ExtractedAsset, + self, param: &mut SystemParamItem, - ) -> Result>; + ) -> Result>; +} + +/// Whether or not to unload the [`RenderAsset`] after extracting it to the render world. +/// +/// Unloading the asset saves on memory, as for most cases it is no longer necessary to keep +/// it in RAM once it's been uploaded to the GPU's VRAM. However, this means you can no longer +/// access the asset from the CPU (via the `Assets` resource) once unloaded (without re-loading it). +/// +/// If you never need access to the asset from the CPU past the first frame it's loaded on, +/// or only need very infrequent access, then set this to Unload. Otherwise, set this to Keep. +#[derive(Reflect, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Debug)] +pub enum RenderAssetPersistencePolicy { + Unload, + #[default] + Keep, } /// This plugin extracts the changed assets from the "app world" into the "render world" @@ -104,7 +122,7 @@ impl RenderAssetDependency for A { /// Temporarily stores the extracted and removed assets of the current frame. #[derive(Resource)] pub struct ExtractedAssets { - extracted: Vec<(AssetId, A::ExtractedAsset)>, + extracted: Vec<(AssetId, A)>, removed: Vec>, } @@ -160,19 +178,21 @@ impl RenderAssets { /// This system extracts all created or modified assets of the corresponding [`RenderAsset`] type /// into the "render world". -fn extract_render_asset( - mut commands: Commands, - mut events: Extract>>, - assets: Extract>>, -) { +fn extract_render_asset(mut commands: Commands, mut main_world: ResMut) { + let mut system_state: SystemState<(EventReader>, ResMut>)> = + SystemState::new(&mut main_world); + let (mut events, mut assets) = system_state.get_mut(&mut main_world); + let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); for event in events.read() { + #[allow(clippy::match_same_arms)] match event { AssetEvent::Added { id } | AssetEvent::Modified { id } => { changed_assets.insert(*id); } - AssetEvent::Removed { id } => { + AssetEvent::Removed { .. } => {} + AssetEvent::Unused { id } => { changed_assets.remove(id); removed.push(*id); } @@ -185,7 +205,13 @@ fn extract_render_asset( let mut extracted_assets = Vec::new(); for id in changed_assets.drain() { if let Some(asset) = assets.get(id) { - extracted_assets.push((id, asset.extract_asset())); + if asset.persistence_policy() == RenderAssetPersistencePolicy::Unload { + if let Some(asset) = assets.remove(id) { + extracted_assets.push((id, asset)); + } + } else { + extracted_assets.push((id, asset.clone())); + } } } @@ -199,7 +225,7 @@ fn extract_render_asset( /// All assets that should be prepared next frame. #[derive(Resource)] pub struct PrepareNextFrameAssets { - assets: Vec<(AssetId, A::ExtractedAsset)>, + assets: Vec<(AssetId, A)>, } impl Default for PrepareNextFrameAssets { @@ -212,16 +238,16 @@ impl Default for PrepareNextFrameAssets { /// This system prepares all assets of the corresponding [`RenderAsset`] type /// which where extracted this frame for the GPU. -pub fn prepare_assets( - mut extracted_assets: ResMut>, - mut render_assets: ResMut>, - mut prepare_next_frame: ResMut>, - param: StaticSystemParam<::Param>, +pub fn prepare_assets( + mut extracted_assets: ResMut>, + mut render_assets: ResMut>, + mut prepare_next_frame: ResMut>, + param: StaticSystemParam<::Param>, ) { let mut param = param.into_inner(); let queued_assets = std::mem::take(&mut prepare_next_frame.assets); for (id, extracted_asset) in queued_assets { - match R::prepare_asset(extracted_asset, &mut param) { + match extracted_asset.prepare_asset(&mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); } @@ -231,12 +257,12 @@ pub fn prepare_assets( } } - for removed in std::mem::take(&mut extracted_assets.removed) { + for removed in extracted_assets.removed.drain(..) { render_assets.remove(removed); } - for (id, extracted_asset) in std::mem::take(&mut extracted_assets.extracted) { - match R::prepare_asset(extracted_asset, &mut param) { + for (id, extracted_asset) in extracted_assets.extracted.drain(..) { + match extracted_asset.prepare_asset(&mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); } diff --git a/crates/bevy_render/src/render_graph/app.rs b/crates/bevy_render/src/render_graph/app.rs index 185260f95b7d9..3d301348a339e 100644 --- a/crates/bevy_render/src/render_graph/app.rs +++ b/crates/bevy_render/src/render_graph/app.rs @@ -32,6 +32,14 @@ pub trait RenderGraphApp { } impl RenderGraphApp for App { + fn add_render_sub_graph(&mut self, sub_graph_name: &'static str) -> &mut Self { + let mut render_graph = self.world.get_resource_mut::().expect( + "RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp", + ); + render_graph.add_sub_graph(sub_graph_name, RenderGraph::default()); + self + } + fn add_render_graph_node( &mut self, sub_graph_name: &'static str, @@ -81,12 +89,4 @@ impl RenderGraphApp for App { } self } - - fn add_render_sub_graph(&mut self, sub_graph_name: &'static str) -> &mut Self { - let mut render_graph = self.world.get_resource_mut::().expect( - "RenderGraph not found. Make sure you are using add_render_sub_graph on the RenderApp", - ); - render_graph.add_sub_graph(sub_graph_name, RenderGraph::default()); - self - } } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index d1eee845bf99f..b293ca620c948 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -142,22 +142,28 @@ impl DrawFunctions

{ /// Compared to the draw function the required ECS data is fetched automatically /// (by the [`RenderCommandState`]) from the render world. /// Therefore the three types [`Param`](RenderCommand::Param), -/// [`ViewData`](RenderCommand::ViewData) and -/// [`ItemData`](RenderCommand::ItemData) are used. +/// [`ViewQuery`](RenderCommand::ViewQuery) and +/// [`ItemQuery`](RenderCommand::ItemQuery) are used. /// They specify which information is required to execute the render command. /// /// Multiple render commands can be combined together by wrapping them in a tuple. /// /// # Example -/// The `DrawPbr` draw function is created from the following render command +/// +/// The `DrawMaterial` draw function is created from the following render command /// tuple. Const generics are used to set specific bind group locations: /// -/// ```ignore -/// pub type DrawPbr = ( +/// ``` +/// # use bevy_render::render_phase::SetItemPipeline; +/// # struct SetMeshViewBindGroup; +/// # struct SetMeshBindGroup; +/// # struct SetMaterialBindGroup(core::marker::PhantomData); +/// # struct DrawMesh; +/// pub type DrawMaterial = ( /// SetItemPipeline, /// SetMeshViewBindGroup<0>, -/// SetStandardMaterialBindGroup<1>, -/// SetTransformBindGroup<2>, +/// SetMeshBindGroup<1>, +/// SetMaterialBindGroup, /// DrawMesh, /// ); /// ``` @@ -179,19 +185,19 @@ pub trait RenderCommand { /// The view entity refers to the camera, or shadow-casting light, etc. from which the phase /// item will be rendered from. /// All components have to be accessed read only. - type ViewData: ReadOnlyQueryData; + type ViewQuery: ReadOnlyQueryData; /// Specifies the ECS data of the item entity required by [`RenderCommand::render`]. /// /// The item is the entity that will be rendered for the corresponding view. /// All components have to be accessed read only. - type ItemData: ReadOnlyQueryData; + type ItemQuery: ReadOnlyQueryData; /// Renders a [`PhaseItem`] by recording commands (e.g. setting pipelines, binding bind groups, /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewData>, - entity: ROQueryItem<'w, Self::ItemData>, + view: ROQueryItem<'w, Self::ViewQuery>, + entity: ROQueryItem<'w, Self::ItemQuery>, param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult; @@ -207,14 +213,14 @@ macro_rules! render_command_tuple_impl { ($(($name: ident, $view: ident, $entity: ident)),*) => { impl),*> RenderCommand

for ($($name,)*) { type Param = ($($name::Param,)*); - type ViewData = ($($name::ViewData,)*); - type ItemData = ($($name::ItemData,)*); + type ViewQuery = ($($name::ViewQuery,)*); + type ItemQuery = ($($name::ItemQuery,)*); #[allow(non_snake_case)] fn render<'w>( _item: &P, - ($($view,)*): ROQueryItem<'w, Self::ViewData>, - ($($entity,)*): ROQueryItem<'w, Self::ItemData>, + ($($view,)*): ROQueryItem<'w, Self::ViewQuery>, + ($($entity,)*): ROQueryItem<'w, Self::ItemQuery>, ($($name,)*): SystemParamItem<'w, '_, Self::Param>, _pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -231,12 +237,12 @@ all_tuples!(render_command_tuple_impl, 0, 15, C, V, E); /// Wraps a [`RenderCommand`] into a state so that it can be used as a [`Draw`] function. /// -/// The [`RenderCommand::Param`], [`RenderCommand::ViewData`] and -/// [`RenderCommand::ItemData`] are fetched from the ECS and passed to the command. +/// The [`RenderCommand::Param`], [`RenderCommand::ViewQuery`] and +/// [`RenderCommand::ItemQuery`] are fetched from the ECS and passed to the command. pub struct RenderCommandState> { state: SystemState, - view: QueryState, - entity: QueryState, + view: QueryState, + entity: QueryState, } impl> RenderCommandState { diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index 144f8327ea8d9..0468754af62ee 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -269,7 +269,7 @@ impl<'a> TrackedRenderPass<'a> { /// /// The structure expected in `indirect_buffer` is the following: /// - /// ```rust + /// ``` /// #[repr(C)] /// struct DrawIndirect { /// vertex_count: u32, // The number of vertices to draw. @@ -292,7 +292,7 @@ impl<'a> TrackedRenderPass<'a> { /// /// The structure expected in `indirect_buffer` is the following: /// - /// ```rust + /// ``` /// #[repr(C)] /// struct DrawIndexedIndirect { /// vertex_count: u32, // The number of vertices to draw. @@ -320,7 +320,7 @@ impl<'a> TrackedRenderPass<'a> { /// /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: /// - /// ```rust + /// ``` /// #[repr(C)] /// struct DrawIndirect { /// vertex_count: u32, // The number of vertices to draw. @@ -358,7 +358,7 @@ impl<'a> TrackedRenderPass<'a> { /// /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: /// - /// ```rust + /// ``` /// #[repr(C)] /// struct DrawIndirect { /// vertex_count: u32, // The number of vertices to draw. @@ -401,7 +401,7 @@ impl<'a> TrackedRenderPass<'a> { /// /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: /// - /// ```rust + /// ``` /// #[repr(C)] /// struct DrawIndexedIndirect { /// vertex_count: u32, // The number of vertices to draw. @@ -441,7 +441,7 @@ impl<'a> TrackedRenderPass<'a> { /// /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: /// - /// ```rust + /// ``` /// #[repr(C)] /// struct DrawIndexedIndirect { /// vertex_count: u32, // The number of vertices to draw. diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 5cf9976ec4d93..df804bfad10b9 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -196,8 +196,8 @@ pub struct SetItemPipeline; impl RenderCommand

for SetItemPipeline { type Param = SRes; - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( item: &P, diff --git a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs index 9f317d86ef0bf..fab0aa19b5c9d 100644 --- a/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/batched_uniform_buffer.rs @@ -89,7 +89,7 @@ impl BatchedUniformBuffer { } pub fn flush(&mut self) { - self.uniforms.push(self.temp.clone()); + self.uniforms.push(&self.temp); self.current_offset += align_to_next(self.temp.size().get(), self.dynamic_offset_alignment as u64) as u32; diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index fc4f5c0d7d608..03e98abbd8818 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -87,6 +87,8 @@ impl Deref for BindGroup { /// values: Vec, /// #[storage(4, read_only, buffer)] /// buffer: Buffer, +/// #[storage_texture(5)] +/// storage_texture: Handle, /// } /// ``` /// @@ -97,6 +99,7 @@ impl Deref for BindGroup { /// @group(2) @binding(1) var color_texture: texture_2d; /// @group(2) @binding(2) var color_sampler: sampler; /// @group(2) @binding(3) var values: array; +/// @group(2) @binding(5) var storage_texture: texture_storage_2d; /// ``` /// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups /// are generally bound to group 2. @@ -123,6 +126,19 @@ impl Deref for BindGroup { /// | `multisampled` = ... | `true`, `false` | `false` | /// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | /// +/// * `storage_texture(BINDING_INDEX, arguments)` +/// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Texture`](crate::render_resource::Texture) +/// GPU resource, which will be bound as a storage texture in shaders. The field will be assumed to implement [`Into>>`]. In practice, +/// most fields should be a [`Handle`](bevy_asset::Handle) or [`Option>`]. If the value of an [`Option>`] is +/// [`None`], the [`FallbackImage`] resource will be used instead. +/// +/// | Arguments | Values | Default | +/// |------------------------|--------------------------------------------------------------------------------------------|---------------| +/// | `dimension` = "..." | `"1d"`, `"2d"`, `"2d_array"`, `"3d"`, `"cube"`, `"cube_array"` | `"2d"` | +/// | `image_format` = ... | any member of [`TextureFormat`](crate::render_resource::TextureFormat) | `Rgba8Unorm` | +/// | `access` = ... | any member of [`StorageTextureAccess`](crate::render_resource::StorageTextureAccess) | `ReadWrite` | +/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `compute` | +/// /// * `sampler(BINDING_INDEX, arguments)` /// * This field's [`Handle`](bevy_asset::Handle) will be used to look up the matching [`Sampler`] GPU /// resource, which will be bound as a sampler in shaders. The field will be assumed to implement [`Into>>`]. In practice, 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 09336eeb0a093..33260f985067d 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -6,7 +6,7 @@ use super::{Sampler, TextureView}; /// Helper for constructing bindgroups. /// /// Allows constructing the descriptor's entries as: -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group( /// "my_bind_group", /// &my_layout, @@ -19,7 +19,7 @@ use super::{Sampler, TextureView}; /// /// instead of /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group( /// "my_bind_group", /// &my_layout, @@ -38,7 +38,7 @@ use super::{Sampler, TextureView}; /// /// or /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group( /// "my_bind_group", /// &my_layout, @@ -51,7 +51,7 @@ use super::{Sampler, TextureView}; /// /// instead of /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group( /// "my_bind_group", /// &my_layout, @@ -70,7 +70,7 @@ use super::{Sampler, TextureView}; /// /// or /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group( /// "my_bind_group", /// &my_layout, @@ -80,7 +80,7 @@ use super::{Sampler, TextureView}; /// /// instead of /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group( /// "my_bind_group", /// &my_layout, 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 1cd7869f92ab7..f9d897159b54a 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 @@ -5,7 +5,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// Helper for constructing bind group layouts. /// /// Allows constructing the layout's entries as: -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// let layout = render_device.create_bind_group_layout( /// "my_bind_group_layout", /// &BindGroupLayoutEntries::with_indices( @@ -23,7 +23,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// /// instead of /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// let layout = render_device.create_bind_group_layout( /// "my_bind_group_layout", /// &[ @@ -51,7 +51,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// /// or /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group_layout( /// "my_bind_group_layout", /// &BindGroupLayoutEntries::sequential( @@ -68,7 +68,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// /// instead of /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// let layout = render_device.create_bind_group_layout( /// "my_bind_group_layout", /// &[ @@ -96,7 +96,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// /// or /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// render_device.create_bind_group_layout( /// "my_bind_group_layout", /// &BindGroupLayoutEntries::single( @@ -108,7 +108,7 @@ use wgpu::{BindGroupLayoutEntry, BindingType, ShaderStages}; /// /// instead of /// -/// ```ignore +/// ```ignore (render_device cannot be easily accessed) /// let layout = render_device.create_bind_group_layout( /// "my_bind_group_layout", /// &[ 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 6c8103be04c30..dbfe6a5962231 100644 --- a/crates/bevy_render/src/render_resource/gpu_array_buffer.rs +++ b/crates/bevy_render/src/render_resource/gpu_array_buffer.rs @@ -9,7 +9,7 @@ use crate::{ use bevy_ecs::{prelude::Component, system::Resource}; use bevy_utils::nonmax::NonMaxU32; use encase::{private::WriteInto, ShaderSize, ShaderType}; -use std::{marker::PhantomData, mem}; +use std::marker::PhantomData; use wgpu::BindingResource; /// Trait for types able to go in a [`GpuArrayBuffer`]. @@ -32,7 +32,7 @@ impl GpuArrayBufferable for T {} #[derive(Resource)] pub enum GpuArrayBuffer { Uniform(BatchedUniformBuffer), - Storage((StorageBuffer>, Vec)), + Storage(StorageBuffer>), } impl GpuArrayBuffer { @@ -41,21 +41,22 @@ impl GpuArrayBuffer { if limits.max_storage_buffers_per_shader_stage == 0 { GpuArrayBuffer::Uniform(BatchedUniformBuffer::new(&limits)) } else { - GpuArrayBuffer::Storage((StorageBuffer::default(), Vec::new())) + GpuArrayBuffer::Storage(StorageBuffer::default()) } } pub fn clear(&mut self) { match self { GpuArrayBuffer::Uniform(buffer) => buffer.clear(), - GpuArrayBuffer::Storage((_, buffer)) => buffer.clear(), + GpuArrayBuffer::Storage(buffer) => buffer.get_mut().clear(), } } pub fn push(&mut self, value: T) -> GpuArrayBufferIndex { match self { GpuArrayBuffer::Uniform(buffer) => buffer.push(value), - GpuArrayBuffer::Storage((_, buffer)) => { + GpuArrayBuffer::Storage(buffer) => { + let buffer = buffer.get_mut(); let index = NonMaxU32::new(buffer.len() as u32).unwrap(); buffer.push(value); GpuArrayBufferIndex { @@ -70,10 +71,7 @@ impl GpuArrayBuffer { pub fn write_buffer(&mut self, device: &RenderDevice, queue: &RenderQueue) { match self { GpuArrayBuffer::Uniform(buffer) => buffer.write_buffer(device, queue), - GpuArrayBuffer::Storage((buffer, vec)) => { - buffer.set(mem::take(vec)); - buffer.write_buffer(device, queue); - } + GpuArrayBuffer::Storage(buffer) => buffer.write_buffer(device, queue), } } @@ -93,7 +91,7 @@ impl GpuArrayBuffer { pub fn binding(&self) -> Option { match self { GpuArrayBuffer::Uniform(buffer) => buffer.binding(), - GpuArrayBuffer::Storage((buffer, _)) => buffer.binding(), + GpuArrayBuffer::Storage(buffer) => buffer.binding(), } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index b90a81472f582..eaeacf5b23608 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -32,23 +32,24 @@ pub use uniform_buffer::*; // TODO: decide where re-exports should go pub use wgpu::{ - util::BufferInitDescriptor, AdapterInfo as WgpuAdapterInfo, AddressMode, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, - BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, - BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, - ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, - ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, Extent3d, Face, Features as WgpuFeatures, FilterMode, - FragmentState as RawFragmentState, FrontFace, ImageCopyBuffer, ImageCopyBufferBase, - ImageCopyTexture, ImageCopyTextureBase, ImageDataLayout, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, - PipelineLayout, PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, - PushConstantRange, RenderPassColorAttachment, RenderPassDepthStencilAttachment, - RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, - SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, - ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp, - TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, - TextureUsages, TextureViewDescriptor, TextureViewDimension, VertexAttribute, + util::{BufferInitDescriptor, DrawIndexedIndirect}, + AdapterInfo as WgpuAdapterInfo, AddressMode, BindGroupDescriptor, BindGroupEntry, + BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, BlendComponent, + BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, + BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, + CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, + ComputePipelineDescriptor as RawComputePipelineDescriptor, DepthBiasState, DepthStencilState, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageCopyBuffer, ImageCopyBufferBase, ImageCopyTexture, ImageCopyTextureBase, + ImageDataLayout, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, + MapMode, MultisampleState, Operations, Origin3d, PipelineLayout, PipelineLayoutDescriptor, + PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, + RenderPipelineDescriptor as RawRenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, + ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, + StencilOperation, StencilState, StorageTextureAccess, StoreOp, TextureAspect, + TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, + TextureViewDescriptor, TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, VertexStepMode, }; diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index bb6d9212b729e..d7197cbd515a7 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -848,6 +848,7 @@ impl PipelineCache { mut events: Extract>>, ) { for event in events.read() { + #[allow(clippy::match_same_arms)] match event { AssetEvent::Added { id } | AssetEvent::Modified { id } => { if let Some(shader) = shaders.get(*id) { @@ -855,6 +856,7 @@ impl PipelineCache { } } AssetEvent::Removed { id } => cache.remove_shader(*id), + AssetEvent::Unused { .. } => {} AssetEvent::LoadedWithDependencies { .. } => { // TODO: handle this } diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index 7b10677784c8f..677378cc90f16 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -213,7 +213,12 @@ impl From<&Source> for naga_oil::compose::ShaderLanguage { fn from(value: &Source) -> Self { match value { Source::Wgsl(_) => naga_oil::compose::ShaderLanguage::Wgsl, + #[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] Source::Glsl(_, _) => naga_oil::compose::ShaderLanguage::Glsl, + #[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] + Source::Glsl(_, _) => panic!( + "GLSL is not supported in this configuration; use the feature `shader_format_glsl`" + ), Source::SpirV(_) => panic!("spirv not yet implemented"), } } @@ -223,13 +228,16 @@ impl From<&Source> for naga_oil::compose::ShaderType { fn from(value: &Source) -> Self { match value { Source::Wgsl(_) => naga_oil::compose::ShaderType::Wgsl, - Source::Glsl(_, naga::ShaderStage::Vertex) => naga_oil::compose::ShaderType::GlslVertex, - Source::Glsl(_, naga::ShaderStage::Fragment) => { - naga_oil::compose::ShaderType::GlslFragment - } - Source::Glsl(_, naga::ShaderStage::Compute) => { - panic!("glsl compute not yet implemented") - } + #[cfg(any(feature = "shader_format_glsl", target_arch = "wasm32"))] + Source::Glsl(_, shader_stage) => match shader_stage { + naga::ShaderStage::Vertex => naga_oil::compose::ShaderType::GlslVertex, + naga::ShaderStage::Fragment => naga_oil::compose::ShaderType::GlslFragment, + naga::ShaderStage::Compute => panic!("glsl compute not yet implemented"), + }, + #[cfg(all(not(feature = "shader_format_glsl"), not(target_arch = "wasm32")))] + Source::Glsl(_, _) => panic!( + "GLSL is not supported in this configuration; use the feature `shader_format_glsl`" + ), Source::SpirV(_) => panic!("spirv not yet implemented"), } } diff --git a/crates/bevy_render/src/render_resource/uniform_buffer.rs b/crates/bevy_render/src/render_resource/uniform_buffer.rs index 2a22cd93529fb..7666e7f6ddd0f 100644 --- a/crates/bevy_render/src/render_resource/uniform_buffer.rs +++ b/crates/bevy_render/src/render_resource/uniform_buffer.rs @@ -227,8 +227,8 @@ impl DynamicUniformBuffer { /// Push data into the `DynamicUniformBuffer`'s internal vector (residing on system RAM). #[inline] - pub fn push(&mut self, value: T) -> u32 { - self.scratch.write(&value).unwrap() as u32 + pub fn push(&mut self, value: &T) -> u32 { + self.scratch.write(value).unwrap() as u32 } pub fn set_label(&mut self, label: Option<&str>) { diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index c046ef11a17be..077d0df8e938b 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -1,8 +1,11 @@ use bevy_ecs::{prelude::Entity, world::World}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -use bevy_utils::HashMap; -use smallvec::{smallvec, SmallVec}; +use bevy_utils::{ + smallvec::{smallvec, SmallVec}, + HashMap, +}; + #[cfg(feature = "trace")] use std::ops::Deref; use std::{borrow::Cow, collections::VecDeque}; diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 7a606e3027e00..9e09dfba9ce52 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -71,6 +71,10 @@ pub fn render_system(world: &mut World) { for window in windows.values_mut() { if let Some(wrapped_texture) = window.swap_chain_texture.take() { if let Some(surface_texture) = wrapped_texture.try_unwrap() { + // TODO(clean): winit docs recommends calling pre_present_notify before this. + // though `present()` doesn't present the frame, it schedules it to be presented + // by wgpu. + // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify surface_texture.present(); } } diff --git a/crates/bevy_render/src/texture/compressed_image_saver.rs b/crates/bevy_render/src/texture/compressed_image_saver.rs index a5f296b566a65..89414ea603948 100644 --- a/crates/bevy_render/src/texture/compressed_image_saver.rs +++ b/crates/bevy_render/src/texture/compressed_image_saver.rs @@ -56,6 +56,7 @@ impl AssetSaver for CompressedImageSaver { format: ImageFormatSetting::Format(ImageFormat::Basis), is_srgb, sampler: image.sampler.clone(), + cpu_persistent_access: image.cpu_persistent_access, }) } .boxed() diff --git a/crates/bevy_render/src/texture/exr_texture_loader.rs b/crates/bevy_render/src/texture/exr_texture_loader.rs index e5494e9935d52..925a468a51aa3 100644 --- a/crates/bevy_render/src/texture/exr_texture_loader.rs +++ b/crates/bevy_render/src/texture/exr_texture_loader.rs @@ -1,10 +1,14 @@ -use crate::texture::{Image, TextureFormatPixelInfo}; +use crate::{ + render_asset::RenderAssetPersistencePolicy, + texture::{Image, TextureFormatPixelInfo}, +}; use bevy_asset::{ io::{AsyncReadExt, Reader}, AssetLoader, LoadContext, }; use bevy_utils::BoxedFuture; use image::ImageDecoder; +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat}; @@ -12,6 +16,11 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat}; #[derive(Clone, Default)] pub struct ExrTextureLoader; +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct ExrTextureLoaderSettings { + pub cpu_persistent_access: RenderAssetPersistencePolicy, +} + /// Possible errors that can be produced by [`ExrTextureLoader`] #[non_exhaustive] #[derive(Debug, Error)] @@ -24,13 +33,13 @@ pub enum ExrTextureLoaderError { impl AssetLoader for ExrTextureLoader { type Asset = Image; - type Settings = (); + type Settings = ExrTextureLoaderSettings; type Error = ExrTextureLoaderError; fn load<'a>( &'a self, reader: &'a mut Reader, - _settings: &'a Self::Settings, + settings: &'a Self::Settings, _load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result> { Box::pin(async move { @@ -63,6 +72,7 @@ impl AssetLoader for ExrTextureLoader { TextureDimension::D2, buf, format, + settings.cpu_persistent_access, )) }) } diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index e4afc211a744c..911881f12fd8c 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -1,4 +1,6 @@ -use crate::{render_resource::*, texture::DefaultImageSampler}; +use crate::{ + render_asset::RenderAssetPersistencePolicy, render_resource::*, texture::DefaultImageSampler, +}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::{FromWorld, Res, ResMut}, @@ -76,7 +78,13 @@ fn fallback_image_new( let image_dimension = dimension.compatible_texture_dimension(); let mut image = if create_texture_with_data { let data = vec![value; format.pixel_size()]; - Image::new_fill(extents, image_dimension, &data, format) + Image::new_fill( + extents, + image_dimension, + &data, + format, + RenderAssetPersistencePolicy::Unload, + ) } else { let mut image = Image::default(); image.texture_descriptor.dimension = TextureDimension::D2; diff --git a/crates/bevy_render/src/texture/hdr_texture_loader.rs b/crates/bevy_render/src/texture/hdr_texture_loader.rs index e54b38b806606..5358b6fb440f2 100644 --- a/crates/bevy_render/src/texture/hdr_texture_loader.rs +++ b/crates/bevy_render/src/texture/hdr_texture_loader.rs @@ -1,5 +1,9 @@ -use crate::texture::{Image, TextureFormatPixelInfo}; +use crate::{ + render_asset::RenderAssetPersistencePolicy, + texture::{Image, TextureFormatPixelInfo}, +}; use bevy_asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext}; +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat}; @@ -7,6 +11,11 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat}; #[derive(Clone, Default)] pub struct HdrTextureLoader; +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct HdrTextureLoaderSettings { + pub cpu_persistent_access: RenderAssetPersistencePolicy, +} + #[non_exhaustive] #[derive(Debug, Error)] pub enum HdrTextureLoaderError { @@ -18,12 +27,12 @@ pub enum HdrTextureLoaderError { impl AssetLoader for HdrTextureLoader { type Asset = Image; - type Settings = (); + type Settings = HdrTextureLoaderSettings; type Error = HdrTextureLoaderError; fn load<'a>( &'a self, reader: &'a mut Reader, - _settings: &'a (), + settings: &'a Self::Settings, _load_context: &'a mut LoadContext, ) -> bevy_utils::BoxedFuture<'a, Result> { Box::pin(async move { @@ -59,6 +68,7 @@ impl AssetLoader for HdrTextureLoader { TextureDimension::D2, rgba_data, format, + settings.cpu_persistent_access, )) }) } diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 6e31b5892c760..a774321e83efa 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -6,7 +6,7 @@ use super::dds::*; use super::ktx2::*; use crate::{ - render_asset::{PrepareAssetError, RenderAsset}, + render_asset::{PrepareAssetError, RenderAsset, RenderAssetPersistencePolicy}, render_resource::{Sampler, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::BevyDefault, @@ -110,6 +110,7 @@ pub struct Image { /// The [`ImageSampler`] to use during rendering. pub sampler: ImageSampler, pub texture_view_descriptor: Option>, + pub cpu_persistent_access: RenderAssetPersistencePolicy, } /// Used in [`Image`], this determines what image sampler to use when rendering. The default setting, @@ -148,6 +149,8 @@ pub struct DefaultImageSampler(pub(crate) Sampler); /// How edges should be handled in texture addressing. /// +/// See [`ImageSamplerDescriptor`] for information how to configure this. +/// /// This type mirrors [`wgpu::AddressMode`]. #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)] pub enum ImageAddressMode { @@ -464,6 +467,7 @@ impl Default for Image { }, sampler: ImageSampler::Default, texture_view_descriptor: None, + cpu_persistent_access: RenderAssetPersistencePolicy::Keep, } } } @@ -479,6 +483,7 @@ impl Image { dimension: TextureDimension, data: Vec, format: TextureFormat, + cpu_persistent_access: RenderAssetPersistencePolicy, ) -> Self { debug_assert_eq!( size.volume() * format.pixel_size(), @@ -492,6 +497,7 @@ impl Image { image.texture_descriptor.dimension = dimension; image.texture_descriptor.size = size; image.texture_descriptor.format = format; + image.cpu_persistent_access = cpu_persistent_access; image } @@ -505,10 +511,12 @@ impl Image { dimension: TextureDimension, pixel: &[u8], format: TextureFormat, + cpu_persistent_access: RenderAssetPersistencePolicy, ) -> Self { let mut value = Image::default(); value.texture_descriptor.format = format; value.texture_descriptor.dimension = dimension; + value.cpu_persistent_access = cpu_persistent_access; value.resize(size); debug_assert_eq!( @@ -629,7 +637,9 @@ impl Image { } _ => None, }) - .map(|(dyn_img, is_srgb)| Self::from_dynamic(dyn_img, is_srgb)) + .map(|(dyn_img, is_srgb)| { + Self::from_dynamic(dyn_img, is_srgb, self.cpu_persistent_access) + }) } /// Load a bytes buffer in a [`Image`], according to type `image_type`, using the `image` @@ -640,6 +650,7 @@ impl Image { #[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats, is_srgb: bool, image_sampler: ImageSampler, + cpu_persistent_access: RenderAssetPersistencePolicy, ) -> Result { let format = image_type.to_image_format()?; @@ -668,7 +679,7 @@ impl Image { reader.set_format(image_crate_format); reader.no_limits(); let dyn_img = reader.decode()?; - Self::from_dynamic(dyn_img, is_srgb) + Self::from_dynamic(dyn_img, is_srgb, cpu_persistent_access) } }; image.sampler = image_sampler; @@ -801,7 +812,6 @@ pub struct GpuImage { } impl RenderAsset for Image { - type ExtractedAsset = Image; type PreparedAsset = GpuImage; type Param = ( SRes, @@ -809,34 +819,32 @@ impl RenderAsset for Image { SRes, ); - /// Clones the Image. - fn extract_asset(&self) -> Self::ExtractedAsset { - self.clone() + fn persistence_policy(&self) -> RenderAssetPersistencePolicy { + self.cpu_persistent_access } /// Converts the extracted image into a [`GpuImage`]. fn prepare_asset( - image: Self::ExtractedAsset, + self, (render_device, render_queue, default_sampler): &mut SystemParamItem, - ) -> Result> { + ) -> Result> { let texture = render_device.create_texture_with_data( render_queue, - &image.texture_descriptor, - &image.data, + &self.texture_descriptor, + &self.data, ); let texture_view = texture.create_view( - image - .texture_view_descriptor + self.texture_view_descriptor .or_else(|| Some(TextureViewDescriptor::default())) .as_ref() .unwrap(), ); let size = Vec2::new( - image.texture_descriptor.size.width as f32, - image.texture_descriptor.size.height as f32, + self.texture_descriptor.size.width as f32, + self.texture_descriptor.size.height as f32, ); - let sampler = match image.sampler { + let sampler = match self.sampler { ImageSampler::Default => (***default_sampler).clone(), ImageSampler::Descriptor(descriptor) => { render_device.create_sampler(&descriptor.as_wgpu()) @@ -846,10 +854,10 @@ impl RenderAsset for Image { Ok(GpuImage { texture, texture_view, - texture_format: image.texture_descriptor.format, + texture_format: self.texture_descriptor.format, sampler, size, - mip_level_count: image.texture_descriptor.mip_level_count, + mip_level_count: self.texture_descriptor.mip_level_count, }) } } @@ -859,9 +867,9 @@ bitflags::bitflags! { #[repr(transparent)] pub struct CompressedImageFormats: u32 { const NONE = 0; - const ASTC_LDR = (1 << 0); - const BC = (1 << 1); - const ETC2 = (1 << 2); + const ASTC_LDR = 1 << 0; + const BC = 1 << 1; + const ETC2 = 1 << 2; } } @@ -916,6 +924,7 @@ impl CompressedImageFormats { mod test { use super::*; + use crate::render_asset::RenderAssetPersistencePolicy; #[test] fn image_size() { @@ -929,6 +938,7 @@ mod test { TextureDimension::D2, &[0, 0, 0, 255], TextureFormat::Rgba8Unorm, + RenderAssetPersistencePolicy::Unload, ); assert_eq!( Vec2::new(size.width as f32, size.height as f32), diff --git a/crates/bevy_render/src/texture/image_loader.rs b/crates/bevy_render/src/texture/image_loader.rs index ca1df0b3b3d60..46c8c739b1f2a 100644 --- a/crates/bevy_render/src/texture/image_loader.rs +++ b/crates/bevy_render/src/texture/image_loader.rs @@ -3,6 +3,7 @@ use bevy_ecs::prelude::{FromWorld, World}; use thiserror::Error; use crate::{ + render_asset::RenderAssetPersistencePolicy, renderer::RenderDevice, texture::{Image, ImageFormat, ImageType, TextureError}, }; @@ -57,6 +58,7 @@ pub struct ImageLoaderSettings { pub format: ImageFormatSetting, pub is_srgb: bool, pub sampler: ImageSampler, + pub cpu_persistent_access: RenderAssetPersistencePolicy, } impl Default for ImageLoaderSettings { @@ -65,6 +67,7 @@ impl Default for ImageLoaderSettings { format: ImageFormatSetting::default(), is_srgb: true, sampler: ImageSampler::Default, + cpu_persistent_access: RenderAssetPersistencePolicy::Keep, } } } @@ -104,6 +107,7 @@ impl AssetLoader for ImageLoader { self.supported_compressed_formats, settings.is_srgb, settings.sampler.clone(), + settings.cpu_persistent_access, ) .map_err(|err| FileTextureError { error: err, diff --git a/crates/bevy_render/src/texture/image_texture_conversion.rs b/crates/bevy_render/src/texture/image_texture_conversion.rs index 298c39219c0cc..999cacc005672 100644 --- a/crates/bevy_render/src/texture/image_texture_conversion.rs +++ b/crates/bevy_render/src/texture/image_texture_conversion.rs @@ -1,11 +1,18 @@ -use crate::texture::{Image, TextureFormatPixelInfo}; +use crate::{ + render_asset::RenderAssetPersistencePolicy, + texture::{Image, TextureFormatPixelInfo}, +}; use image::{DynamicImage, ImageBuffer}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat}; impl Image { /// Converts a [`DynamicImage`] to an [`Image`]. - pub fn from_dynamic(dyn_img: DynamicImage, is_srgb: bool) -> Image { + pub fn from_dynamic( + dyn_img: DynamicImage, + is_srgb: bool, + cpu_persistent_access: RenderAssetPersistencePolicy, + ) -> Image { use bevy_core::cast_slice; let width; let height; @@ -151,6 +158,7 @@ impl Image { TextureDimension::D2, data, format, + cpu_persistent_access, ) } @@ -214,6 +222,7 @@ mod test { use image::{GenericImage, Rgba}; use super::*; + use crate::render_asset::RenderAssetPersistencePolicy; #[test] fn two_way_conversion() { @@ -221,7 +230,8 @@ mod test { let mut initial = DynamicImage::new_rgba8(1, 1); initial.put_pixel(0, 0, Rgba::from([132, 3, 7, 200])); - let image = Image::from_dynamic(initial.clone(), true); + let image = + Image::from_dynamic(initial.clone(), true, RenderAssetPersistencePolicy::Unload); // NOTE: Fails if `is_srbg = false` or the dynamic image is of the type rgb8. assert_eq!(initial, image.try_into_dynamic().unwrap()); diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index d7a31a2bebf27..866cbc928c68e 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -14,6 +14,7 @@ mod image; mod image_loader; #[cfg(feature = "ktx2")] mod ktx2; +mod texture_attachment; mod texture_cache; pub(crate) mod image_texture_conversion; @@ -32,6 +33,7 @@ pub use hdr_texture_loader::*; pub use compressed_image_saver::*; pub use fallback_image::*; pub use image_loader::*; +pub use texture_attachment::*; pub use texture_cache::*; use crate::{ diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs new file mode 100644 index 0000000000000..908d2e3a2869a --- /dev/null +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -0,0 +1,123 @@ +use super::CachedTexture; +use crate::{prelude::Color, render_resource::TextureView}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use wgpu::{ + LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, +}; + +/// A wrapper for a [`CachedTexture`] that is used as a [`RenderPassColorAttachment`]. +#[derive(Clone)] +pub struct ColorAttachment { + pub texture: CachedTexture, + pub resolve_target: Option, + clear_color: Color, + is_first_call: Arc, +} + +impl ColorAttachment { + pub fn new( + texture: CachedTexture, + resolve_target: Option, + clear_color: Color, + ) -> Self { + Self { + texture, + resolve_target, + clear_color, + is_first_call: Arc::new(AtomicBool::new(true)), + } + } + + /// Get this texture view as an attachment. The attachment will be cleared with a value of + /// `clear_color` if this is the first time calling this function, otherwise it will be loaded. + /// + /// The returned attachment will always have writing enabled (`store: StoreOp::Load`). + pub fn get_attachment(&self) -> RenderPassColorAttachment { + if let Some(resolve_target) = self.resolve_target.as_ref() { + let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); + + RenderPassColorAttachment { + view: &resolve_target.default_view, + resolve_target: Some(&self.texture.default_view), + ops: Operations { + load: if first_call { + LoadOp::Clear(self.clear_color.into()) + } else { + LoadOp::Load + }, + store: StoreOp::Store, + }, + } + } else { + self.get_unsampled_attachment() + } + } + + /// Get this texture view as an attachment, without the resolve target. The attachment will be cleared with + /// a value of `clear_color` if this is the first time calling this function, otherwise it will be loaded. + /// + /// The returned attachment will always have writing enabled (`store: StoreOp::Load`). + pub fn get_unsampled_attachment(&self) -> RenderPassColorAttachment { + let first_call = self.is_first_call.fetch_and(false, Ordering::SeqCst); + + RenderPassColorAttachment { + view: &self.texture.default_view, + resolve_target: None, + ops: Operations { + load: if first_call { + LoadOp::Clear(self.clear_color.into()) + } else { + LoadOp::Load + }, + store: StoreOp::Store, + }, + } + } + + pub(crate) fn mark_as_cleared(&self) { + self.is_first_call.store(false, Ordering::SeqCst); + } +} + +/// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`]. +pub struct DepthAttachment { + pub view: TextureView, + clear_value: Option, + is_first_call: Arc, +} + +impl DepthAttachment { + pub fn new(view: TextureView, clear_value: Option) -> Self { + Self { + view, + clear_value, + is_first_call: Arc::new(AtomicBool::new(clear_value.is_some())), + } + } + + /// Get this texture view as an attachment. The attachment will be cleared with a value of + /// `clear_value` if this is the first time calling this function with `store` == [`StoreOp::Store`], + /// and a clear value was provided, otherwise it will be loaded. + pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment { + let first_call = self + .is_first_call + .fetch_and(store != StoreOp::Store, Ordering::SeqCst); + + RenderPassDepthStencilAttachment { + view: &self.view, + depth_ops: Some(Operations { + load: if first_call { + // If first_call is true, then a clear value will always have been provided in the constructor + LoadOp::Clear(self.clear_value.unwrap()) + } else { + LoadOp::Load + }, + store, + }), + stencil_ops: None, + } + } +} diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 7a0b928d7cced..acaa1818b27d0 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -6,7 +6,10 @@ pub use visibility::*; pub use window::*; use crate::{ - camera::{ExtractedCamera, ManualTextureViews, MipBias, TemporalJitter}, + camera::{ + CameraMainTextureUsages, ClearColor, ClearColorConfig, ExposureSettings, ExtractedCamera, + ManualTextureViews, MipBias, TemporalJitter, + }, extract_resource::{ExtractResource, ExtractResourcePlugin}, prelude::{Image, Shader}, primitives::Frustum, @@ -14,7 +17,7 @@ use crate::{ render_phase::ViewRangefinder3d, render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, - texture::{BevyDefault, CachedTexture, TextureCache}, + texture::{BevyDefault, CachedTexture, ColorAttachment, DepthAttachment, TextureCache}, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; @@ -28,8 +31,8 @@ use std::sync::{ Arc, }; use wgpu::{ - Color, Extent3d, Operations, RenderPassColorAttachment, TextureDescriptor, TextureDimension, - TextureFormat, TextureUsages, + Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp, + TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, }; pub const VIEW_TYPE_HANDLE: Handle = Handle::weak_from_u128(15421373904451797197); @@ -59,7 +62,8 @@ impl Plugin for ViewPlugin { prepare_view_targets .in_set(RenderSet::ManageViews) .after(prepare_windows) - .after(crate::render_asset::prepare_assets::), + .after(crate::render_asset::prepare_assets::) + .ambiguous_with(crate::camera::sort_cameras), // doesn't use `sorted_camera_index_for_target` prepare_view_uniforms.in_set(RenderSet::PrepareResources), ), ); @@ -167,6 +171,7 @@ pub struct ViewUniform { projection: Mat4, inverse_projection: Mat4, world_position: Vec3, + exposure: f32, // viewport(x_origin, y_origin, width, height) viewport: Vec4, frustum: [Vec4; 6], @@ -204,40 +209,30 @@ pub struct PostProcessWrite<'a> { impl ViewTarget { pub const TEXTURE_FORMAT_HDR: TextureFormat = TextureFormat::Rgba16Float; - /// Retrieve this target's color attachment. This will use [`Self::sampled_main_texture_view`] and resolve to [`Self::main_texture`] if - /// the target has sampling enabled. Otherwise it will use [`Self::main_texture`] directly. - pub fn get_color_attachment(&self, ops: Operations) -> RenderPassColorAttachment { - match &self.main_textures.sampled { - Some(CachedTexture { - default_view: sampled_texture_view, - .. - }) => RenderPassColorAttachment { - view: sampled_texture_view, - resolve_target: Some(self.main_texture_view()), - ops, - }, - None => self.get_unsampled_color_attachment(ops), + /// Retrieve this target's main texture's color attachment. + pub fn get_color_attachment(&self) -> RenderPassColorAttachment { + if self.main_texture.load(Ordering::SeqCst) == 0 { + self.main_textures.a.get_attachment() + } else { + self.main_textures.b.get_attachment() } } - /// Retrieve an "unsampled" color attachment using [`Self::main_texture`]. - pub fn get_unsampled_color_attachment( - &self, - ops: Operations, - ) -> RenderPassColorAttachment { - RenderPassColorAttachment { - view: self.main_texture_view(), - resolve_target: None, - ops, + /// Retrieve this target's "unsampled" main texture's color attachment. + pub fn get_unsampled_color_attachment(&self) -> RenderPassColorAttachment { + if self.main_texture.load(Ordering::SeqCst) == 0 { + self.main_textures.a.get_unsampled_attachment() + } else { + self.main_textures.b.get_unsampled_attachment() } } /// The "main" unsampled texture. pub fn main_texture(&self) -> &Texture { if self.main_texture.load(Ordering::SeqCst) == 0 { - &self.main_textures.a.texture + &self.main_textures.a.texture.texture } else { - &self.main_textures.b.texture + &self.main_textures.b.texture.texture } } @@ -249,18 +244,18 @@ impl ViewTarget { /// ahead of time. pub fn main_texture_other(&self) -> &Texture { if self.main_texture.load(Ordering::SeqCst) == 0 { - &self.main_textures.b.texture + &self.main_textures.b.texture.texture } else { - &self.main_textures.a.texture + &self.main_textures.a.texture.texture } } /// The "main" unsampled texture. pub fn main_texture_view(&self) -> &TextureView { if self.main_texture.load(Ordering::SeqCst) == 0 { - &self.main_textures.a.default_view + &self.main_textures.a.texture.default_view } else { - &self.main_textures.b.default_view + &self.main_textures.b.texture.default_view } } @@ -272,16 +267,17 @@ impl ViewTarget { /// ahead of time. pub fn main_texture_other_view(&self) -> &TextureView { if self.main_texture.load(Ordering::SeqCst) == 0 { - &self.main_textures.b.default_view + &self.main_textures.b.texture.default_view } else { - &self.main_textures.a.default_view + &self.main_textures.a.texture.default_view } } /// The "main" sampled texture. pub fn sampled_main_texture(&self) -> Option<&Texture> { self.main_textures - .sampled + .a + .resolve_target .as_ref() .map(|sampled| &sampled.texture) } @@ -289,7 +285,8 @@ impl ViewTarget { /// The "main" sampled texture view. pub fn sampled_main_texture_view(&self) -> Option<&TextureView> { self.main_textures - .sampled + .a + .resolve_target .as_ref() .map(|sampled| &sampled.default_view) } @@ -328,14 +325,16 @@ impl ViewTarget { let old_is_a_main_texture = self.main_texture.fetch_xor(1, Ordering::SeqCst); // if the old main texture is a, then the post processing must write from a to b if old_is_a_main_texture == 0 { + self.main_textures.b.mark_as_cleared(); PostProcessWrite { - source: &self.main_textures.a.default_view, - destination: &self.main_textures.b.default_view, + source: &self.main_textures.a.texture.default_view, + destination: &self.main_textures.b.texture.default_view, } } else { + self.main_textures.a.mark_as_cleared(); PostProcessWrite { - source: &self.main_textures.b.default_view, - destination: &self.main_textures.a.default_view, + source: &self.main_textures.b.texture.default_view, + destination: &self.main_textures.a.texture.default_view, } } } @@ -344,7 +343,24 @@ impl ViewTarget { #[derive(Component)] pub struct ViewDepthTexture { pub texture: Texture, - pub view: TextureView, + attachment: DepthAttachment, +} + +impl ViewDepthTexture { + pub fn new(texture: CachedTexture, clear_value: Option) -> Self { + Self { + texture: texture.texture, + attachment: DepthAttachment::new(texture.default_view, clear_value), + } + } + + pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment { + self.attachment.get_attachment(store) + } + + pub fn view(&self) -> &TextureView { + &self.attachment.view + } } pub fn prepare_view_uniforms( @@ -354,6 +370,7 @@ pub fn prepare_view_uniforms( mut view_uniforms: ResMut, views: Query<( Entity, + Option<&ExtractedCamera>, &ExtractedView, Option<&Frustum>, Option<&TemporalJitter>, @@ -370,9 +387,18 @@ pub fn prepare_view_uniforms( else { return; }; - for (entity, camera, frustum, temporal_jitter, mip_bias, maybe_layers) in &views { - let viewport = camera.viewport.as_vec4(); - let unjittered_projection = camera.projection; + for ( + entity, + extracted_camera, + extracted_view, + frustum, + temporal_jitter, + mip_bias, + maybe_layers, + ) in &views + { + let viewport = extracted_view.viewport.as_vec4(); + let unjittered_projection = extracted_view.projection; let mut projection = unjittered_projection; if let Some(temporal_jitter) = temporal_jitter { @@ -380,13 +406,13 @@ pub fn prepare_view_uniforms( } let inverse_projection = projection.inverse(); - let view = camera.transform.compute_matrix(); + let view = extracted_view.transform.compute_matrix(); let inverse_view = view.inverse(); let view_proj = if temporal_jitter.is_some() { projection * inverse_view } else { - camera + extracted_view .view_projection .unwrap_or_else(|| projection * inverse_view) }; @@ -405,10 +431,13 @@ pub fn prepare_view_uniforms( inverse_view, projection, inverse_projection, - world_position: camera.transform.translation(), + world_position: extracted_view.transform.translation(), + exposure: extracted_camera + .map(|c| c.exposure) + .unwrap_or_else(|| ExposureSettings::default().exposure()), viewport, frustum, - color_grading: camera.color_grading, + color_grading: extracted_view.color_grading, mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0, render_layers: maybe_layers.copied().unwrap_or_default().bits(), }), @@ -420,9 +449,8 @@ pub fn prepare_view_uniforms( #[derive(Clone)] struct MainTargetTextures { - a: CachedTexture, - b: CachedTexture, - sampled: Option, + a: ColorAttachment, + b: ColorAttachment, /// 0 represents `main_textures.a`, 1 represents `main_textures.b` /// This is shared across view targets with the same render target main_texture: Arc, @@ -434,13 +462,19 @@ fn prepare_view_targets( windows: Res, images: Res>, msaa: Res, + clear_color_global: Res, render_device: Res, mut texture_cache: ResMut, - cameras: Query<(Entity, &ExtractedCamera, &ExtractedView)>, + cameras: Query<( + Entity, + &ExtractedCamera, + &ExtractedView, + &CameraMainTextureUsages, + )>, manual_texture_views: Res, ) { let mut textures = HashMap::default(); - for (entity, camera, view) in cameras.iter() { + for (entity, camera, view, texture_usage) in cameras.iter() { if let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) { if let (Some(out_texture_view), Some(out_texture_format)) = ( target.get_texture_view(&windows, &images, &manual_texture_views), @@ -458,7 +492,12 @@ fn prepare_view_targets( TextureFormat::bevy_default() }; - let main_textures = textures + let clear_color = match camera.clear_color { + ClearColorConfig::Custom(color) => color, + _ => clear_color_global.0, + }; + + let (a, b, sampled) = textures .entry((camera.target.clone(), view.hdr)) .or_insert_with(|| { let descriptor = TextureDescriptor { @@ -468,9 +507,7 @@ fn prepare_view_targets( sample_count: 1, dimension: TextureDimension::D2, format: main_texture_format, - usage: TextureUsages::RENDER_ATTACHMENT - | TextureUsages::TEXTURE_BINDING - | TextureUsages::COPY_SRC, + usage: texture_usage.0, view_formats: match main_texture_format { TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], @@ -509,18 +546,19 @@ fn prepare_view_targets( } else { None }; - MainTargetTextures { - a, - b, - sampled, - main_texture: Arc::new(AtomicUsize::new(0)), - } + (a, b, sampled) }); + let main_textures = MainTargetTextures { + a: ColorAttachment::new(a.clone(), sampled.clone(), clear_color), + b: ColorAttachment::new(b.clone(), sampled.clone(), clear_color), + main_texture: Arc::new(AtomicUsize::new(0)), + }; + commands.entity(entity).insert(ViewTarget { - main_textures: main_textures.clone(), - main_texture_format, main_texture: main_textures.main_texture.clone(), + main_textures, + main_texture_format, out_texture: out_texture_view.clone(), out_texture_format: out_texture_format.add_srgb_suffix(), }); diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index a48fb19f56382..237113b713a0d 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -16,6 +16,7 @@ struct View { projection: mat4x4, inverse_projection: mat4x4, world_position: vec3, + exposure: f32, // viewport(x_origin, y_origin, width, height) viewport: vec4, frustum: array, 6>, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index a92d2fdeda0fd..4427b408e127c 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -12,6 +12,7 @@ use bevy_transform::{components::GlobalTransform, TransformSystem}; use std::cell::Cell; use thread_local::ThreadLocal; +use crate::deterministic::DeterministicRenderingConfig; use crate::{ camera::{ camera_system, Camera, CameraProjection, OrthographicProjection, PerspectiveProjection, @@ -392,6 +393,7 @@ pub fn check_visibility( &GlobalTransform, Has, )>, + deterministic_rendering_config: Res, ) { for (mut visible_entities, frustum, maybe_view_mask, camera) in &mut view_query { if !camera.is_active { @@ -452,6 +454,11 @@ pub fn check_visibility( for cell in &mut thread_queues { visible_entities.entities.append(cell.get_mut()); } + if deterministic_rendering_config.stable_sort_z_fighting { + // We can use the faster unstable sort here because + // the values (`Entity`) are guaranteed to be unique. + visible_entities.entities.sort_unstable(); + } } } diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index aac968940d5ee..aec0e53d1aee3 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -8,7 +8,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; -use bevy_utils::{default, tracing::debug, HashMap, HashSet}; +use bevy_utils::{default, tracing::debug, EntityHashMap, HashSet}; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, }; @@ -89,11 +89,11 @@ impl ExtractedWindow { #[derive(Default, Resource)] pub struct ExtractedWindows { pub primary: Option, - pub windows: HashMap, + pub windows: EntityHashMap, } impl Deref for ExtractedWindows { - type Target = HashMap; + type Target = EntityHashMap; fn deref(&self) -> &Self::Target { &self.windows @@ -199,7 +199,7 @@ struct SurfaceData { #[derive(Resource, Default)] pub struct WindowSurfaces { - surfaces: HashMap, + surfaces: EntityHashMap, /// List of windows that we have already called the initial `configure_surface` for configured_windows: HashSet, } diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 6f87a178c2326..7238df17a7635 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -5,7 +5,7 @@ use bevy_asset::{load_internal_asset, Handle}; use bevy_ecs::prelude::*; use bevy_log::{error, info, info_span}; use bevy_tasks::AsyncComputeTaskPool; -use bevy_utils::HashMap; +use bevy_utils::EntityHashMap; use std::sync::Mutex; use thiserror::Error; use wgpu::{ @@ -14,6 +14,7 @@ use wgpu::{ use crate::{ prelude::{Image, Shader}, + render_asset::RenderAssetPersistencePolicy, render_resource::{ binding_types::texture_2d, BindGroup, BindGroupLayout, BindGroupLayoutEntries, Buffer, CachedRenderPipelineId, FragmentState, PipelineCache, RenderPipelineDescriptor, @@ -32,7 +33,7 @@ pub type ScreenshotFn = Box; #[derive(Resource, Default)] pub struct ScreenshotManager { // this is in a mutex to enable extraction with only an immutable reference - pub(crate) callbacks: Mutex>, + pub(crate) callbacks: Mutex>, } #[derive(Error, Debug)] @@ -363,6 +364,7 @@ pub(crate) fn collect_screenshots(world: &mut World) { wgpu::TextureDimension::D2, result, texture_format, + RenderAssetPersistencePolicy::Unload, )); }; diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index d071dc8b49754..676fb9fdcd834 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -139,7 +139,7 @@ impl DynamicScene { // If the entity already has the given component attached, // just apply the (possibly) new value, otherwise add the // component to the entity. - reflect_component.apply_or_insert(entity_mut, &**component); + reflect_component.apply_or_insert(entity_mut, &**component, &type_registry); } } @@ -193,7 +193,7 @@ where #[cfg(test)] mod tests { use bevy_ecs::{reflect::AppTypeRegistry, system::Command, world::World}; - use bevy_hierarchy::{AddChild, Parent}; + use bevy_hierarchy::{Parent, PushChild}; use bevy_utils::EntityHashMap; use crate::dynamic_scene_builder::DynamicSceneBuilder; @@ -211,7 +211,7 @@ mod tests { .register::(); let original_parent_entity = world.spawn_empty().id(); let original_child_entity = world.spawn_empty().id(); - AddChild { + PushChild { parent: original_parent_entity, child: original_child_entity, } @@ -232,7 +232,7 @@ mod tests { // We then add the parent from the scene as a child of the original child // Hierarchy should look like: // Original Parent <- Original Child <- Scene Parent <- Scene Child - AddChild { + PushChild { parent: original_child_entity, child: from_scene_parent_entity, } diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index d09df9d0c7119..28cf824aa061c 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -96,7 +96,7 @@ impl Scene { for scene_entity in archetype.entities() { let entity = *instance_info .entity_map - .entry(scene_entity.entity()) + .entry(scene_entity.id()) .or_insert_with(|| world.spawn_empty().id()); for component_id in archetype.components() { let component_info = self @@ -117,7 +117,13 @@ impl Scene { } }) })?; - reflect_component.copy(&self.world, world, scene_entity.entity(), entity); + reflect_component.copy( + &self.world, + world, + scene_entity.id(), + entity, + &type_registry, + ); } } } diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 33dc7c2ea9d42..fc42c522f16ee 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ system::{Command, Resource}, world::{Mut, World}, }; -use bevy_hierarchy::{AddChild, Parent}; +use bevy_hierarchy::{Parent, PushChild}; use bevy_utils::{tracing::error, EntityHashMap, HashMap, HashSet}; use thiserror::Error; use uuid::Uuid; @@ -198,7 +198,7 @@ impl SceneSpawner { &mut self, world: &mut World, id: impl Into>, - ) -> Result<(), SceneSpawnError> { + ) -> Result { let mut entity_map = EntityHashMap::default(); let id = id.into(); Self::spawn_dynamic_internal(world, id, &mut entity_map)?; @@ -207,7 +207,7 @@ impl SceneSpawner { .insert(instance_id, InstanceInfo { entity_map }); let spawned = self.spawned_dynamic_scenes.entry(id).or_default(); spawned.push(instance_id); - Ok(()) + Ok(instance_id) } fn spawn_dynamic_internal( @@ -348,7 +348,7 @@ impl SceneSpawner { // this case shouldn't happen anyway .unwrap_or(true) { - AddChild { + PushChild { parent, child: entity, } @@ -434,3 +434,67 @@ pub fn scene_spawner_system(world: &mut World) { scene_spawner.set_scene_instance_parent_sync(world); }); } + +#[cfg(test)] +mod tests { + use super::*; + use bevy_ecs::component::Component; + use bevy_ecs::entity::Entity; + use bevy_ecs::prelude::ReflectComponent; + use bevy_ecs::query::With; + use bevy_ecs::{reflect::AppTypeRegistry, world::World}; + + use crate::DynamicSceneBuilder; + use bevy_reflect::Reflect; + + #[derive(Reflect, Component, Debug, PartialEq, Eq, Clone, Copy, Default)] + #[reflect(Component)] + struct A(usize); + + #[test] + fn clone_dynamic_entities() { + let mut world = World::default(); + + // setup + let atr = AppTypeRegistry::default(); + atr.write().register::(); + world.insert_resource(atr); + world.insert_resource(Assets::::default()); + + // start test + world.spawn(A(42)); + + assert_eq!(world.query::<&A>().iter(&world).len(), 1); + + // clone only existing entity + let mut scene_spawner = SceneSpawner::default(); + let entity = world.query_filtered::>().single(&world); + let scene = DynamicSceneBuilder::from_world(&world) + .extract_entity(entity) + .build(); + + let scene_id = world.resource_mut::>().add(scene); + let instance_id = scene_spawner + .spawn_dynamic_sync(&mut world, scene_id) + .unwrap(); + + // verify we spawned exactly one new entity with our expected component + assert_eq!(world.query::<&A>().iter(&world).len(), 2); + + // verify that we can get this newly-spawned entity by the instance ID + let new_entity = scene_spawner + .iter_instance_entities(instance_id) + .next() + .unwrap(); + + // verify this is not the original entity + assert_ne!(entity, new_entity); + + // verify this new entity contains the same data as the original entity + let [old_a, new_a] = world + .query::<&A>() + .get_many(&world, [entity, new_entity]) + .unwrap(); + assert_eq!(old_a, new_a); + } +} diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index 6ca7d0e301630..0567a9451b617 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -236,6 +236,28 @@ impl<'a, 'de> Visitor<'de> for SceneVisitor<'a> { formatter.write_str("scene struct") } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let resources = seq + .next_element_seed(SceneMapDeserializer { + registry: self.type_registry, + })? + .ok_or_else(|| Error::missing_field(SCENE_RESOURCES))?; + + let entities = seq + .next_element_seed(SceneEntitiesDeserializer { + type_registry: self.type_registry, + })? + .ok_or_else(|| Error::missing_field(SCENE_ENTITIES))?; + + Ok(DynamicScene { + resources, + entities, + }) + } + fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, @@ -271,28 +293,6 @@ impl<'a, 'de> Visitor<'de> for SceneVisitor<'a> { entities, }) } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let resources = seq - .next_element_seed(SceneMapDeserializer { - registry: self.type_registry, - })? - .ok_or_else(|| Error::missing_field(SCENE_RESOURCES))?; - - let entities = seq - .next_element_seed(SceneEntitiesDeserializer { - type_registry: self.type_registry, - })? - .ok_or_else(|| Error::missing_field(SCENE_ENTITIES))?; - - Ok(DynamicScene { - resources, - entities, - }) - } } /// Handles deserialization for a collection of entities. @@ -455,6 +455,20 @@ impl<'a, 'de> Visitor<'de> for SceneMapVisitor<'a> { formatter.write_str("map of reflect types") } + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut dynamic_properties = Vec::new(); + while let Some(entity) = + seq.next_element_seed(UntypedReflectDeserializer::new(self.registry))? + { + dynamic_properties.push(entity); + } + + Ok(dynamic_properties) + } + fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de>, @@ -478,20 +492,6 @@ impl<'a, 'de> Visitor<'de> for SceneMapVisitor<'a> { Ok(entries) } - - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut dynamic_properties = Vec::new(); - while let Some(entity) = - seq.next_element_seed(UntypedReflectDeserializer::new(self.registry))? - { - dynamic_properties.push(entity); - } - - Ok(dynamic_properties) - } } #[cfg(test)] @@ -605,18 +605,18 @@ mod tests { ), }, entities: { - 0: ( + 4294967296: ( components: { "bevy_scene::serde::tests::Foo": (123), }, ), - 1: ( + 4294967297: ( components: { "bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Bar": (345), }, ), - 2: ( + 4294967298: ( components: { "bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Bar": (345), @@ -642,18 +642,18 @@ mod tests { ), }, entities: { - 0: ( + 4294967296: ( components: { "bevy_scene::serde::tests::Foo": (123), }, ), - 1: ( + 4294967297: ( components: { "bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Bar": (345), }, ), - 2: ( + 4294967298: ( components: { "bevy_scene::serde::tests::Foo": (123), "bevy_scene::serde::tests::Bar": (345), @@ -763,10 +763,10 @@ mod tests { assert_eq!( vec![ - 0, 1, 0, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, - 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, - 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, 108, 64, 1, 12, 72, - 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 0, 1, 128, 128, 128, 128, 16, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, + 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, + 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, + 108, 64, 1, 12, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 ], serialized_scene ); @@ -803,11 +803,11 @@ mod tests { assert_eq!( vec![ - 146, 128, 129, 0, 145, 129, 217, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, - 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, - 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, 63, 166, - 102, 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, 108, 101, 172, 72, 101, - 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 146, 128, 129, 207, 0, 0, 0, 1, 0, 0, 0, 0, 145, 129, 217, 37, 98, 101, 118, 121, + 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, + 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, + 2, 3, 146, 202, 63, 166, 102, 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, + 108, 101, 172, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 ], buf ); @@ -844,7 +844,7 @@ mod tests { assert_eq!( vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index b2f639481159b..efe3337070299 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -1,7 +1,4 @@ -use crate::{ - texture_atlas::{TextureAtlas, TextureAtlasSprite}, - Sprite, -}; +use crate::{texture_atlas::TextureAtlas, ImageScaleMode, Sprite}; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; use bevy_render::{ @@ -15,6 +12,8 @@ use bevy_transform::components::{GlobalTransform, Transform}; pub struct SpriteBundle { /// Specifies the rendering properties of the sprite, such as color tint and flip. pub sprite: Sprite, + /// Controls how the image is altered when scaled. + pub scale_mode: ImageScaleMode, /// The local transform of the sprite, relative to its parent. pub transform: Transform, /// The absolute transform of the sprite. This should generally not be written to directly. @@ -30,16 +29,25 @@ pub struct SpriteBundle { } /// A [`Bundle`] of components for drawing a single sprite from a sprite sheet (also referred -/// to as a `TextureAtlas`). +/// to as a `TextureAtlas`) or for animated sprites. +/// +/// Note: +/// This bundle is identical to [`SpriteBundle`] with an additional [`TextureAtlas`] component. +/// +/// Check the following examples for usage: +/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) +/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) #[derive(Bundle, Clone, Default)] pub struct SpriteSheetBundle { - /// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0. - pub sprite: TextureAtlasSprite, - /// A handle to the texture atlas that holds the sprite images - pub texture_atlas: Handle, - /// Data pertaining to how the sprite is drawn on the screen + pub sprite: Sprite, + /// Controls how the image is altered when scaled. + pub scale_mode: ImageScaleMode, pub transform: Transform, pub global_transform: GlobalTransform, + /// The sprite sheet base texture + pub texture: Handle, + /// The sprite sheet texture atlas, allowing to draw a custom section of `texture`. + pub atlas: TextureAtlas, /// User indication of whether an entity is visible pub visibility: Visibility, pub inherited_visibility: InheritedVisibility, diff --git a/crates/bevy_sprite/src/collide_aabb.rs b/crates/bevy_sprite/src/collide_aabb.rs index 52d9fd1152e1e..cc9c7c6c4bf41 100644 --- a/crates/bevy_sprite/src/collide_aabb.rs +++ b/crates/bevy_sprite/src/collide_aabb.rs @@ -12,8 +12,29 @@ pub enum Collision { Inside, } +struct CollisionBox { + pub top: f32, + pub bottom: f32, + pub left: f32, + pub right: f32, +} + +impl CollisionBox { + pub fn new(pos: Vec3, size: Vec2) -> Self { + Self { + top: pos.y + size.y / 2., + bottom: pos.y - size.y / 2., + left: pos.x - size.x / 2., + right: pos.x + size.x / 2., + } + } +} + // TODO: ideally we can remove this once bevy gets a physics system -/// Axis-aligned bounding box collision with "side" detection +/// Axis-aligned bounding box collision with "side" detection. +/// +/// The [Collision], in case it occurred, is the side of `b` where `a` hit. +/// /// * `a_pos` and `b_pos` are the center positions of the rectangles, typically obtained by /// extracting the `translation` field from a [`Transform`](bevy_transform::components::Transform) component /// * `a_size` and `b_size` are the dimensions (width and height) of the rectangles. @@ -23,30 +44,25 @@ pub enum Collision { /// If the collision occurs on multiple sides, the side with the shallowest penetration is returned. /// If all sides are involved, [`Collision::Inside`] is returned. pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option { - let a_min = a_pos.truncate() - a_size / 2.0; - let a_max = a_pos.truncate() + a_size / 2.0; - - let b_min = b_pos.truncate() - b_size / 2.0; - let b_max = b_pos.truncate() + b_size / 2.0; + let a = CollisionBox::new(a_pos, a_size); + let b = CollisionBox::new(b_pos, b_size); // check to see if the two rectangles are intersecting - if a_min.x < b_max.x && a_max.x > b_min.x && a_min.y < b_max.y && a_max.y > b_min.y { + if a.left < b.right && a.right > b.left && a.bottom < b.top && a.top > b.bottom { // check to see if we hit on the left or right side - let (x_collision, x_depth) = if a_min.x < b_min.x && a_max.x > b_min.x && a_max.x < b_max.x - { - (Collision::Left, b_min.x - a_max.x) - } else if a_min.x > b_min.x && a_min.x < b_max.x && a_max.x > b_max.x { - (Collision::Right, a_min.x - b_max.x) + let (x_collision, x_depth) = if a.left < b.left && a.right > b.left && a.right < b.right { + (Collision::Left, b.left - a.right) + } else if a.left > b.left && a.left < b.right && a.right > b.right { + (Collision::Right, a.left - b.right) } else { (Collision::Inside, -f32::INFINITY) }; // check to see if we hit on the top or bottom side - let (y_collision, y_depth) = if a_min.y < b_min.y && a_max.y > b_min.y && a_max.y < b_max.y - { - (Collision::Bottom, b_min.y - a_max.y) - } else if a_min.y > b_min.y && a_min.y < b_max.y && a_max.y > b_max.y { - (Collision::Top, a_min.y - b_max.y) + let (y_collision, y_depth) = if a.bottom < b.bottom && a.top > b.bottom && a.top < b.top { + (Collision::Bottom, b.bottom - a.top) + } else if a.bottom > b.bottom && a.bottom < b.top && a.top > b.top { + (Collision::Top, a.bottom - b.top) } else { (Collision::Inside, -f32::INFINITY) }; @@ -63,9 +79,98 @@ pub fn collide(a_pos: Vec3, a_size: Vec2, b_pos: Vec3, b_size: Vec2) -> Option) { + let a_size = Vec2::new(30., 50.); + let b_size = Vec2::new(50., 30.); + assert_eq!(collide(a, a_size, b, b_size), expected); + } + fn collide_two_rectangles( // (x, y, size x, size y) a: (f32, f32, f32, f32), diff --git a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs index fcf19bba537ae..e1af48f930432 100644 --- a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs @@ -1,13 +1,16 @@ -use crate::TextureAtlas; -use bevy_asset::Assets; +use crate::TextureAtlasLayout; +use bevy_asset::{Assets, Handle}; use bevy_math::{IVec2, Rect, Vec2}; -use bevy_render::texture::{Image, TextureFormatPixelInfo}; +use bevy_render::{ + render_asset::RenderAssetPersistencePolicy, + texture::{Image, TextureFormatPixelInfo}, +}; use guillotiere::{size2, Allocation, AtlasAllocator}; -/// Helper utility to update [`TextureAtlas`] on the fly. +/// Helper utility to update [`TextureAtlasLayout`] on the fly. /// /// Helpful in cases when texture is created procedurally, -/// e.g: in a font glyph [`TextureAtlas`], only add the [`Image`] texture for letters to be rendered. +/// e.g: in a font glyph [`TextureAtlasLayout`], only add the [`Image`] texture for letters to be rendered. pub struct DynamicTextureAtlasBuilder { atlas_allocator: AtlasAllocator, padding: i32, @@ -27,24 +30,39 @@ impl DynamicTextureAtlasBuilder { } } - /// Add a new texture to [`TextureAtlas`]. - /// It is user's responsibility to pass in the correct [`TextureAtlas`] + /// Add a new texture to `atlas_layout` + /// It is the user's responsibility to pass in the correct [`TextureAtlasLayout`] + /// and that `atlas_texture_handle` has [`Image::cpu_persistent_access`] + /// set to [`RenderAssetPersistencePolicy::Keep`] + /// + /// # Arguments + /// + /// * `altas_layout` - The atlas to add the texture to + /// * `textures` - The texture assets container + /// * `texture` - The new texture to add to the atlas + /// * `atlas_texture_handle` - The atlas texture to edit pub fn add_texture( &mut self, - texture_atlas: &mut TextureAtlas, + atlas_layout: &mut TextureAtlasLayout, textures: &mut Assets, texture: &Image, + atlas_texture_handle: &Handle, ) -> Option { let allocation = self.atlas_allocator.allocate(size2( texture.width() as i32 + self.padding, texture.height() as i32 + self.padding, )); if let Some(allocation) = allocation { - let atlas_texture = textures.get_mut(&texture_atlas.texture).unwrap(); + let atlas_texture = textures.get_mut(atlas_texture_handle).unwrap(); + assert_eq!( + atlas_texture.cpu_persistent_access, + RenderAssetPersistencePolicy::Keep + ); + self.place_texture(atlas_texture, allocation, texture); let mut rect: Rect = to_rect(allocation.rectangle); rect.max -= self.padding as f32; - Some(texture_atlas.add_texture(rect)) + Some(atlas_layout.add_texture(rect)) } else { None } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index e64fb9808da19..3ca92858c314c 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -6,6 +6,7 @@ mod render; mod sprite; mod texture_atlas; mod texture_atlas_builder; +mod texture_slice; pub mod collide_aabb; @@ -13,8 +14,9 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ bundle::{SpriteBundle, SpriteSheetBundle}, - sprite::Sprite, - texture_atlas::{TextureAtlas, TextureAtlasSprite}, + sprite::{ImageScaleMode, Sprite}, + texture_atlas::{TextureAtlas, TextureAtlasLayout}, + texture_slice::{BorderRect, SliceScaleMode, TextureSlicer}, ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder, }; } @@ -26,6 +28,7 @@ pub use render::*; pub use sprite::*; pub use texture_atlas::*; pub use texture_atlas_builder::*; +pub use texture_slice::*; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; @@ -51,6 +54,7 @@ pub const SPRITE_SHADER_HANDLE: Handle = Handle::weak_from_u128(27633439 #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum SpriteSystem { ExtractSprites, + ComputeSlices, } impl Plugin for SpritePlugin { @@ -61,16 +65,25 @@ impl Plugin for SpritePlugin { "render/sprite.wgsl", Shader::from_wgsl ); - app.init_asset::() - .register_asset_reflect::() + app.init_asset::() + .register_asset_reflect::() .register_type::() - .register_type::() + .register_type::() + .register_type::() .register_type::() + .register_type::() .register_type::() .add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin)) .add_systems( PostUpdate, - calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), + ( + calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), + ( + compute_slices_on_asset_event, + compute_slices_on_sprite_change, + ) + .in_set(SpriteSystem::ComputeSlices), + ), ); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { @@ -118,19 +131,15 @@ pub fn calculate_bounds_2d( mut commands: Commands, meshes: Res>, images: Res>, - atlases: Res>, + atlases: Res>, meshes_without_aabb: Query<(Entity, &Mesh2dHandle), (Without, Without)>, sprites_to_recalculate_aabb: Query< - (Entity, &Sprite, &Handle), + (Entity, &Sprite, &Handle, Option<&TextureAtlas>), ( Or<(Without, Changed)>, Without, ), >, - atlases_without_aabb: Query< - (Entity, &TextureAtlasSprite, &Handle), - (Without, Without), - >, ) { for (entity, mesh_handle) in &meshes_without_aabb { if let Some(mesh) = meshes.get(&mesh_handle.0) { @@ -139,27 +148,15 @@ pub fn calculate_bounds_2d( } } } - for (entity, sprite, texture_handle) in &sprites_to_recalculate_aabb { - if let Some(size) = sprite - .custom_size - .or_else(|| images.get(texture_handle).map(|image| image.size_f32())) - { - let aabb = Aabb { - center: (-sprite.anchor.as_vec() * size).extend(0.0).into(), - half_extents: (0.5 * size).extend(0.0).into(), - }; - commands.entity(entity).try_insert(aabb); - } - } - for (entity, atlas_sprite, atlas_handle) in &atlases_without_aabb { - if let Some(size) = atlas_sprite.custom_size.or_else(|| { - atlases - .get(atlas_handle) - .and_then(|atlas| atlas.textures.get(atlas_sprite.index)) - .map(|rect| (rect.min - rect.max).abs()) + for (entity, sprite, texture_handle, atlas) in &sprites_to_recalculate_aabb { + if let Some(size) = sprite.custom_size.or_else(|| match atlas { + // We default to the texture size for regular sprites + None => images.get(texture_handle).map(|image| image.size_f32()), + // We default to the drawn rect for atlas sprites + Some(atlas) => atlas.texture_rect(&atlases).map(|rect| rect.size()), }) { let aabb = Aabb { - center: (-atlas_sprite.anchor.as_vec() * size).extend(0.0).into(), + center: (-sprite.anchor.as_vec() * size).extend(0.0).into(), half_extents: (0.5 * size).extend(0.0).into(), }; commands.entity(entity).try_insert(aabb); @@ -186,7 +183,7 @@ mod test { app.insert_resource(image_assets); let mesh_assets = Assets::::default(); app.insert_resource(mesh_assets); - let texture_atlas_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); app.insert_resource(texture_atlas_assets); // Add system @@ -224,7 +221,7 @@ mod test { app.insert_resource(image_assets); let mesh_assets = Assets::::default(); app.insert_resource(mesh_assets); - let texture_atlas_assets = Assets::::default(); + let texture_atlas_assets = Assets::::default(); app.insert_resource(texture_atlas_assets); // Add system diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 8e5e39b006a00..cbf6baf082317 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -77,7 +77,7 @@ impl From> for ColorMaterial { bitflags::bitflags! { #[repr(transparent)] pub struct ColorMaterialFlags: u32 { - const TEXTURE = (1 << 0); + const TEXTURE = 1 << 0; const NONE = 0; const UNINITIALIZED = 0xFFFF; } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 8fcb83bb3f92a..07626d47a5c9f 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -330,8 +330,8 @@ impl RenderCommand

SRes>, SRes>, ); - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( @@ -354,7 +354,7 @@ impl RenderCommand

} } -const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { +pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { match tonemapping { Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, Tonemapping::Reinhard => Mesh2dPipelineKey::TONEMAP_METHOD_REINHARD, @@ -515,6 +515,7 @@ pub fn extract_materials_2d( let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); for event in events.read() { + #[allow(clippy::match_same_arms)] match event { AssetEvent::Added { id } | AssetEvent::Modified { id } => { changed_assets.insert(*id); @@ -523,7 +524,7 @@ pub fn extract_materials_2d( changed_assets.remove(id); removed.push(*id); } - + AssetEvent::Unused { .. } => {} AssetEvent::LoadedWithDependencies { .. } => { // TODO: handle this } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 03c856068f0fb..4b9c9943dc793 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -5,7 +5,7 @@ use bevy_core_pipeline::core_2d::Transparent2d; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::*, - query::{QueryItem, ROQueryItem}, + query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_math::{Affine3, Vec4}; @@ -339,25 +339,21 @@ impl Mesh2dPipeline { impl GetBatchData for Mesh2dPipeline { type Param = SRes; - type Data = Entity; - type Filter = With; type CompareData = (Material2dBindGroupId, AssetId); type BufferData = Mesh2dUniform; fn get_batch_data( mesh_instances: &SystemParamItem, - entity: &QueryItem, - ) -> (Self::BufferData, Option) { - let mesh_instance = mesh_instances - .get(entity) - .expect("Failed to find render mesh2d instance"); - ( + entity: Entity, + ) -> Option<(Self::BufferData, Option)> { + let mesh_instance = mesh_instances.get(&entity)?; + Some(( (&mesh_instance.transforms).into(), mesh_instance.automatic_batching.then_some(( mesh_instance.material_bind_group_id, mesh_instance.mesh_asset_id, )), - ) + )) } } @@ -369,9 +365,9 @@ bitflags::bitflags! { // FIXME: make normals optional? pub struct Mesh2dPipelineKey: u32 { const NONE = 0; - const HDR = (1 << 0); - const TONEMAP_IN_SHADER = (1 << 1); - const DEBAND_DITHER = (1 << 2); + const HDR = 1 << 0; + const TONEMAP_IN_SHADER = 1 << 1; + const DEBAND_DITHER = 1 << 2; const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; @@ -617,13 +613,13 @@ pub fn prepare_mesh2d_view_bind_groups( pub struct SetMesh2dViewBindGroup; impl RenderCommand

for SetMesh2dViewBindGroup { type Param = (); - type ViewData = (Read, Read); - type ItemData = (); + type ViewQuery = (Read, Read); + type ItemQuery = (); #[inline] fn render<'w>( _item: &P, - (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewData>, + (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, _view: (), _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -637,8 +633,8 @@ impl RenderCommand

for SetMesh2dViewBindGroup; impl RenderCommand

for SetMesh2dBindGroup { type Param = SRes; - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( @@ -666,8 +662,8 @@ impl RenderCommand

for SetMesh2dBindGroup { pub struct DrawMesh2d; impl RenderCommand

for DrawMesh2d { type Param = (SRes>, SRes); - type ViewData = (); - type ItemData = (); + type ViewQuery = (); + type ItemQuery = (); #[inline] fn render<'w>( diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index eaf44fc4ab349..df319a7857e34 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,8 +1,8 @@ use std::ops::Range; use crate::{ - texture_atlas::{TextureAtlas, TextureAtlasSprite}, - Sprite, SPRITE_SHADER_HANDLE, + texture_atlas::{TextureAtlas, TextureAtlasLayout}, + ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE, }; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_core_pipeline::{ @@ -121,10 +121,10 @@ bitflags::bitflags! { // MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. pub struct SpritePipelineKey: u32 { const NONE = 0; - const COLORED = (1 << 0); - const HDR = (1 << 1); - const TONEMAP_IN_SHADER = (1 << 2); - const DEBAND_DITHER = (1 << 3); + const COLORED = 1 << 0; + const HDR = 1 << 1; + const TONEMAP_IN_SHADER = 1 << 2; + const DEBAND_DITHER = 1 << 3; const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const TONEMAP_METHOD_RESERVED_BITS = Self::TONEMAP_METHOD_MASK_BITS << Self::TONEMAP_METHOD_SHIFT_BITS; const TONEMAP_METHOD_NONE = 0 << Self::TONEMAP_METHOD_SHIFT_BITS; @@ -333,8 +333,9 @@ pub fn extract_sprite_events( } pub fn extract_sprites( + mut commands: Commands, mut extracted_sprites: ResMut, - texture_atlases: Extract>>, + texture_atlases: Extract>>, sprite_query: Extract< Query<( Entity, @@ -342,73 +343,38 @@ pub fn extract_sprites( &Sprite, &GlobalTransform, &Handle, - )>, - >, - atlas_query: Extract< - Query<( - Entity, - &ViewVisibility, - &TextureAtlasSprite, - &GlobalTransform, - &Handle, + Option<&TextureAtlas>, + Option<&ComputedTextureSlices>, )>, >, ) { extracted_sprites.sprites.clear(); - - for (entity, view_visibility, sprite, transform, handle) in sprite_query.iter() { + for (entity, view_visibility, sprite, transform, handle, sheet, slices) in sprite_query.iter() { if !view_visibility.get() { continue; } - // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive - extracted_sprites.sprites.insert( - entity, - ExtractedSprite { - color: sprite.color, - transform: *transform, - rect: sprite.rect, - // Pass the custom size - custom_size: sprite.custom_size, - flip_x: sprite.flip_x, - flip_y: sprite.flip_y, - image_handle_id: handle.id(), - anchor: sprite.anchor.as_vec(), - original_entity: None, - }, - ); - } - for (entity, view_visibility, atlas_sprite, transform, texture_atlas_handle) in - atlas_query.iter() - { - if !view_visibility.get() { - continue; - } - if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { - let rect = Some( - *texture_atlas - .textures - .get(atlas_sprite.index) - .unwrap_or_else(|| { - panic!( - "Sprite index {:?} does not exist for texture atlas handle {:?}.", - atlas_sprite.index, - texture_atlas_handle.id(), - ) - }), + + if let Some(slices) = slices { + extracted_sprites.sprites.extend( + slices + .extract_sprites(transform, entity, sprite, handle) + .map(|e| (commands.spawn_empty().id(), e)), ); + } else { + let rect = sheet.and_then(|s| s.texture_rect(&texture_atlases)); + // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive extracted_sprites.sprites.insert( entity, ExtractedSprite { - color: atlas_sprite.color, + color: sprite.color, transform: *transform, - // Select the area in the texture atlas rect, // Pass the custom size - custom_size: atlas_sprite.custom_size, - flip_x: atlas_sprite.flip_x, - flip_y: atlas_sprite.flip_y, - image_handle_id: texture_atlas.texture.id(), - anchor: atlas_sprite.anchor.as_vec(), + custom_size: sprite.custom_size, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + image_handle_id: handle.id(), + anchor: sprite.anchor.as_vec(), original_entity: None, }, ); @@ -588,8 +554,9 @@ pub fn prepare_sprites( // If an image has changed, the GpuImage has (probably) changed for event in &events.images { match event { - AssetEvent::Added {..} | - // images don't have dependencies + AssetEvent::Added { .. } | + AssetEvent::Unused { .. } | + // Images don't have dependencies AssetEvent::LoadedWithDependencies { .. } => {} AssetEvent::Modified { id } | AssetEvent::Removed { id } => { image_bind_groups.values.remove(id); @@ -767,8 +734,8 @@ pub type DrawSprite = ( pub struct SetSpriteViewBindGroup; impl RenderCommand

for SetSpriteViewBindGroup { type Param = SRes; - type ViewData = Read; - type ItemData = (); + type ViewQuery = Read; + type ItemQuery = (); fn render<'w>( _item: &P, @@ -788,8 +755,8 @@ impl RenderCommand

for SetSpriteViewBindGroup; impl RenderCommand

for SetSpriteTextureBindGroup { type Param = SRes; - type ViewData = (); - type ItemData = Read; + type ViewQuery = (); + type ItemQuery = Read; fn render<'w>( _item: &P, @@ -815,8 +782,8 @@ impl RenderCommand

for SetSpriteTextureBindGrou pub struct DrawSpriteBatch; impl RenderCommand

for DrawSpriteBatch { type Param = SRes; - type ViewData = (); - type ItemData = Read; + type ViewQuery = (); + type ItemQuery = Read; fn render<'w>( _item: &P, diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 039f5d722acb7..5c60d759a5468 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -3,6 +3,8 @@ use bevy_math::{Rect, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::color::Color; +use crate::TextureSlicer; + /// Specifies the rendering properties of a sprite. /// /// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle). @@ -26,6 +28,27 @@ pub struct Sprite { pub anchor: Anchor, } +/// Controls how the image is altered when scaled. +#[derive(Component, Debug, Default, Clone, Reflect)] +#[reflect(Component, Default)] +pub enum ImageScaleMode { + /// The entire texture stretches when its dimensions change. This is the default option. + #[default] + Stretched, + /// The texture will be cut in 9 slices, keeping the texture in proportions on resize + Sliced(TextureSlicer), + /// The texture will be repeated if stretched beyond `stretched_value` + Tiled { + /// Should the image repeat horizontally + tile_x: bool, + /// Should the image repeat vertically + tile_y: bool, + /// The texture will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above this value. + stretch_value: f32, + }, +} + /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). /// It defaults to `Anchor::Center`. #[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)] diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index fa1cf53aeb22c..2feed94da0f4f 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -1,97 +1,84 @@ -use crate::Anchor; -use bevy_asset::{Asset, AssetId, Handle}; -use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_asset::{Asset, AssetId, Assets, Handle}; +use bevy_ecs::component::Component; use bevy_math::{Rect, Vec2}; use bevy_reflect::Reflect; -use bevy_render::{color::Color, texture::Image}; +use bevy_render::texture::Image; use bevy_utils::HashMap; -/// An atlas containing multiple textures (like a spritesheet or a tilemap). +/// Stores a map used to lookup the position of a texture in a [`TextureAtlas`]. +/// This can be used to either use and look up a specific section of a texture, or animate frame-by-frame as a sprite sheet. +/// +/// Optionaly it can store a mapping from sub texture handles to the related area index (see +/// [`TextureAtlasBuilder`]). +/// /// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) /// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) +/// +/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder #[derive(Asset, Reflect, Debug, Clone)] #[reflect(Debug)] -pub struct TextureAtlas { - /// The handle to the texture in which the sprites are stored - pub texture: Handle, +pub struct TextureAtlasLayout { // TODO: add support to Uniforms derive to write dimensions and sprites to the same buffer pub size: Vec2, /// The specific areas of the atlas where each texture can be found pub textures: Vec, - /// Mapping from texture handle to index + /// Maps from a specific image handle to the index in `textures` where they can be found. + /// + /// This field is set by [`TextureAtlasBuilder`]. + /// + /// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder pub(crate) texture_handles: Option, usize>>, } -/// Specifies the rendering properties of a sprite from a sprite sheet. +/// Component used to draw a specific section of a texture. +/// +/// It stores a handle to [`TextureAtlasLayout`] and the index of the current section of the atlas. +/// The texture atlas contains various *sections* of a given texture, allowing users to have a single +/// image file for either sprite animation or global mapping. +/// You can change the texture [`index`](Self::index) of the atlas to animate the sprite or dsplay only a *section* of the texture +/// for efficient rendering of related game objects. /// -/// This is commonly used as a component within [`SpriteSheetBundle`](crate::bundle::SpriteSheetBundle). -#[derive(Component, Debug, Clone, Reflect)] -#[reflect(Component)] -pub struct TextureAtlasSprite { - /// The tint color used to draw the sprite, defaulting to [`Color::WHITE`] - pub color: Color, - /// Texture index in [`TextureAtlas`] +/// Check the following examples for usage: +/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) +/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) +#[derive(Component, Default, Debug, Clone, Reflect)] +pub struct TextureAtlas { + /// Texture atlas handle + pub layout: Handle, + /// Texture atlas section index pub index: usize, - /// Whether to flip the sprite in the X axis - pub flip_x: bool, - /// Whether to flip the sprite in the Y axis - pub flip_y: bool, - /// An optional custom size for the sprite that will be used when rendering, instead of the size - /// of the sprite's image in the atlas - pub custom_size: Option, - /// [`Anchor`] point of the sprite in the world - pub anchor: Anchor, } -impl Default for TextureAtlasSprite { - fn default() -> Self { +impl TextureAtlasLayout { + /// Create a new empty layout with custom `dimensions` + pub fn new_empty(dimensions: Vec2) -> Self { Self { - index: 0, - color: Color::WHITE, - flip_x: false, - flip_y: false, - custom_size: None, - anchor: Anchor::default(), - } - } -} - -impl TextureAtlasSprite { - /// Create a new [`TextureAtlasSprite`] with a sprite index, - /// it should be valid in the corresponding [`TextureAtlas`] - pub fn new(index: usize) -> TextureAtlasSprite { - Self { - index, - ..Default::default() - } - } -} - -impl TextureAtlas { - /// Create a new [`TextureAtlas`] that has a texture, but does not have - /// any individual sprites specified - pub fn new_empty(texture: Handle, dimensions: Vec2) -> Self { - Self { - texture, size: dimensions, texture_handles: None, textures: Vec::new(), } } - /// Generate a [`TextureAtlas`] by splitting a texture into a grid where each - /// `tile_size` by `tile_size` grid-cell is one of the textures in the + /// Generate a [`TextureAtlasLayout`] as a grid where each + /// `tile_size` by `tile_size` grid-cell is one of the *section* in the /// atlas. Grid cells are separated by some `padding`, and the grid starts - /// at `offset` pixels from the top left corner. The resulting [`TextureAtlas`] is + /// at `offset` pixels from the top left corner. Resulting layout is /// indexed left to right, top to bottom. + /// + /// # Arguments + /// + /// * `tile_size` - Each layout grid cell size + /// * `columns` - Grid column count + /// * `rows` - Grid row count + /// * `padding` - Optional padding between cells + /// * `offset` - Optional global grid offset pub fn from_grid( - texture: Handle, tile_size: Vec2, columns: usize, rows: usize, padding: Option, offset: Option, - ) -> TextureAtlas { + ) -> Self { let padding = padding.unwrap_or_default(); let offset = offset.unwrap_or_default(); let mut sprites = Vec::new(); @@ -119,37 +106,40 @@ impl TextureAtlas { let grid_size = Vec2::new(columns as f32, rows as f32); - TextureAtlas { + Self { size: ((tile_size + current_padding) * grid_size) - current_padding, textures: sprites, - texture, texture_handles: None, } } - /// Add a sprite to the list of textures in the [`TextureAtlas`] - /// returns an index to the texture which can be used with [`TextureAtlasSprite`] + /// Add a *section* to the list in the layout and returns its index + /// which can be used with [`TextureAtlas`] /// /// # Arguments /// - /// * `rect` - The section of the atlas that contains the texture to be added, - /// from the top-left corner of the texture to the bottom-right corner + /// * `rect` - The section of the texture to be added + /// + /// [`TextureAtlas`]: crate::TextureAtlas pub fn add_texture(&mut self, rect: Rect) -> usize { self.textures.push(rect); self.textures.len() - 1 } - /// The number of textures in the [`TextureAtlas`] + /// The number of textures in the [`TextureAtlasLayout`] pub fn len(&self) -> usize { self.textures.len() } - /// Returns `true` if there are no textures in the [`TextureAtlas`] pub fn is_empty(&self) -> bool { self.textures.is_empty() } - /// Returns the index of the texture corresponding to the given image handle in the [`TextureAtlas`] + /// Retrieves the texture *section* index of the given `texture` handle. + /// + /// This requires the layout to have been built using a [`TextureAtlasBuilder`] + /// + /// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder pub fn get_texture_index(&self, texture: impl Into>) -> Option { let id = texture.into(); self.texture_handles @@ -157,3 +147,20 @@ impl TextureAtlas { .and_then(|texture_handles| texture_handles.get(&id).cloned()) } } + +impl TextureAtlas { + /// Retrieves the current texture [`Rect`] of the sprite sheet according to the section `index` + pub fn texture_rect(&self, texture_atlases: &Assets) -> Option { + let atlas = texture_atlases.get(&self.layout)?; + atlas.textures.get(self.index).copied() + } +} + +impl From> for TextureAtlas { + fn from(texture_atlas: Handle) -> Self { + Self { + layout: texture_atlas, + index: 0, + } + } +} diff --git a/crates/bevy_sprite/src/texture_atlas_builder.rs b/crates/bevy_sprite/src/texture_atlas_builder.rs index 50ae821510b2b..3a8eae428ce0c 100644 --- a/crates/bevy_sprite/src/texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/texture_atlas_builder.rs @@ -1,7 +1,9 @@ +use bevy_asset::Handle; use bevy_asset::{AssetId, Assets}; use bevy_log::{debug, error, warn}; use bevy_math::{Rect, UVec2, Vec2}; use bevy_render::{ + render_asset::RenderAssetPersistencePolicy, render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::{Image, TextureFormatPixelInfo}, }; @@ -12,7 +14,7 @@ use rectangle_pack::{ }; use thiserror::Error; -use crate::texture_atlas::TextureAtlas; +use crate::TextureAtlasLayout; #[derive(Debug, Error)] pub enum TextureAtlasBuilderError { @@ -145,12 +147,41 @@ impl TextureAtlasBuilder { } } - /// Consumes the builder and returns a result with a new texture atlas. + /// Consumes the builder, and returns the newly created texture handle and + /// the assciated atlas layout. /// /// Internally it copies all rectangles from the textures and copies them - /// into a new texture which the texture atlas will use. It is not useful to - /// hold a strong handle to the texture afterwards else it will exist twice - /// in memory. + /// into a new texture. + /// It is not useful to hold a strong handle to the texture afterwards else + /// it will exist twice in memory. + /// + /// # Usage + /// + /// ```rust + /// # use bevy_sprite::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::*; + /// # use bevy_render::prelude::*; + /// + /// fn my_system(mut commands: Commands, mut textures: ResMut>, mut layouts: ResMut>) { + /// // Declare your builder + /// let mut builder = TextureAtlasBuilder::default(); + /// // Customize it + /// // ... + /// // Build your texture and the atlas layout + /// let (atlas_layout, texture) = builder.finish(&mut textures).unwrap(); + /// let layout = layouts.add(atlas_layout); + /// // Spawn your sprite + /// commands.spawn(SpriteSheetBundle { + /// texture, + /// atlas: TextureAtlas { + /// layout, + /// index: 0 + /// }, + /// ..Default::default() + /// }); + /// } + /// ``` /// /// # Errors /// @@ -159,7 +190,7 @@ impl TextureAtlasBuilder { pub fn finish( self, textures: &mut Assets, - ) -> Result { + ) -> Result<(TextureAtlasLayout, Handle), TextureAtlasBuilderError> { let initial_width = self.initial_size.x as u32; let initial_height = self.initial_size.y as u32; let max_width = self.max_size.x as u32; @@ -208,6 +239,7 @@ impl TextureAtlasBuilder { self.format.pixel_size() * (current_width * current_height) as usize ], self.format, + RenderAssetPersistencePolicy::Keep, ); Some(rect_placements) } @@ -246,11 +278,14 @@ impl TextureAtlasBuilder { } self.copy_converted_texture(&mut atlas_texture, texture, packed_location); } - Ok(TextureAtlas { - size: atlas_texture.size_f32(), - texture: textures.add(atlas_texture), - textures: texture_rects, - texture_handles: Some(texture_ids), - }) + + Ok(( + TextureAtlasLayout { + size: atlas_texture.size_f32(), + textures: texture_rects, + texture_handles: Some(texture_ids), + }, + textures.add(atlas_texture), + )) } } diff --git a/crates/bevy_sprite/src/texture_slice/border_rect.rs b/crates/bevy_sprite/src/texture_slice/border_rect.rs new file mode 100644 index 0000000000000..e32f2891c1579 --- /dev/null +++ b/crates/bevy_sprite/src/texture_slice/border_rect.rs @@ -0,0 +1,59 @@ +use bevy_reflect::Reflect; + +/// Struct defining a [`Sprite`](crate::Sprite) border with padding values +#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)] +pub struct BorderRect { + /// Pixel padding to the left + pub left: f32, + /// Pixel padding to the right + pub right: f32, + /// Pixel padding to the top + pub top: f32, + /// Pixel padding to the bottom + pub bottom: f32, +} + +impl BorderRect { + /// Creates a new border as a square, with identical pixel padding values on every direction + #[must_use] + #[inline] + pub const fn square(value: f32) -> Self { + Self { + left: value, + right: value, + top: value, + bottom: value, + } + } + + /// Creates a new border as a rectangle, with: + /// - `horizontal` for left and right pixel padding + /// - `vertical` for top and bottom pixel padding + #[must_use] + #[inline] + pub const fn rectangle(horizontal: f32, vertical: f32) -> Self { + Self { + left: horizontal, + right: horizontal, + top: vertical, + bottom: vertical, + } + } +} + +impl From for BorderRect { + fn from(v: f32) -> Self { + Self::square(v) + } +} + +impl From<[f32; 4]> for BorderRect { + fn from([left, right, top, bottom]: [f32; 4]) -> Self { + Self { + left, + right, + top, + bottom, + } + } +} diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs new file mode 100644 index 0000000000000..dd316f7d3c759 --- /dev/null +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -0,0 +1,150 @@ +use crate::{ExtractedSprite, ImageScaleMode, Sprite}; + +use super::TextureSlice; +use bevy_asset::{AssetEvent, Assets, Handle}; +use bevy_ecs::prelude::*; +use bevy_math::{Rect, Vec2}; +use bevy_render::texture::Image; +use bevy_transform::prelude::*; +use bevy_utils::HashSet; + +/// Component storing texture slices for sprite entities with a tiled or sliced [`ImageScaleMode`] +/// +/// This component is automatically inserted and updated +#[derive(Debug, Clone, Component)] +pub struct ComputedTextureSlices(Vec); + +impl ComputedTextureSlices { + /// Computes [`ExtractedSprite`] iterator from the sprite slices + /// + /// # Arguments + /// + /// * `transform` - the sprite entity global transform + /// * `original_entity` - the sprite entity + /// * `sprite` - The sprite component + /// * `handle` - The sprite texture handle + #[must_use] + pub(crate) fn extract_sprites<'a>( + &'a self, + transform: &'a GlobalTransform, + original_entity: Entity, + sprite: &'a Sprite, + handle: &'a Handle, + ) -> impl ExactSizeIterator + 'a { + self.0.iter().map(move |slice| { + let transform = + transform.mul_transform(Transform::from_translation(slice.offset.extend(0.0))); + ExtractedSprite { + original_entity: Some(original_entity), + color: sprite.color, + transform, + rect: Some(slice.texture_rect), + custom_size: Some(slice.draw_size), + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + image_handle_id: handle.id(), + anchor: sprite.anchor.as_vec(), + } + }) + } +} + +/// Generates sprite slices for a `sprite` given a `scale_mode`. The slices +/// will be computed according to the `image_handle` dimensions or the sprite rect. +/// +/// Returns `None` if either: +/// - The scale mode is [`ImageScaleMode::Stretched`] +/// - The image asset is not loaded +#[must_use] +fn compute_sprite_slices( + sprite: &Sprite, + scale_mode: &ImageScaleMode, + image_handle: &Handle, + images: &Assets, +) -> Option { + if let ImageScaleMode::Stretched = scale_mode { + return None; + } + let image_size = images.get(image_handle).map(|i| { + Vec2::new( + i.texture_descriptor.size.width as f32, + i.texture_descriptor.size.height as f32, + ) + })?; + let slices = match scale_mode { + ImageScaleMode::Stretched => unreachable!(), + ImageScaleMode::Sliced(slicer) => slicer.compute_slices( + sprite.rect.unwrap_or(Rect { + min: Vec2::ZERO, + max: image_size, + }), + sprite.custom_size, + ), + ImageScaleMode::Tiled { + tile_x, + tile_y, + stretch_value, + } => { + let slice = TextureSlice { + texture_rect: sprite.rect.unwrap_or(Rect { + min: Vec2::ZERO, + max: image_size, + }), + draw_size: sprite.custom_size.unwrap_or(image_size), + offset: Vec2::ZERO, + }; + slice.tiled(*stretch_value, (*tile_x, *tile_y)) + } + }; + Some(ComputedTextureSlices(slices)) +} + +/// System reacting to added or modified [`Image`] handles, and recompute sprite slices +/// on matching sprite entities +pub(crate) fn compute_slices_on_asset_event( + mut commands: Commands, + mut events: EventReader>, + images: Res>, + sprites: Query<(Entity, &ImageScaleMode, &Sprite, &Handle)>, +) { + // We store the asset ids of added/modified image assets + let added_handles: HashSet<_> = events + .read() + .filter_map(|e| match e { + AssetEvent::Added { id } | AssetEvent::Modified { id } => Some(*id), + _ => None, + }) + .collect(); + if added_handles.is_empty() { + return; + } + // We recompute the sprite slices for sprite entities with a matching asset handle id + for (entity, scale_mode, sprite, image_handle) in &sprites { + if !added_handles.contains(&image_handle.id()) { + continue; + } + if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) { + commands.entity(entity).insert(slices); + } + } +} + +/// System reacting to changes on relevant sprite bundle components to compute the sprite slices +pub(crate) fn compute_slices_on_sprite_change( + mut commands: Commands, + images: Res>, + changed_sprites: Query< + (Entity, &ImageScaleMode, &Sprite, &Handle), + Or<( + Changed, + Changed>, + Changed, + )>, + >, +) { + for (entity, scale_mode, sprite, image_handle) in &changed_sprites { + if let Some(slices) = compute_sprite_slices(sprite, scale_mode, image_handle, &images) { + commands.entity(entity).insert(slices); + } + } +} diff --git a/crates/bevy_sprite/src/texture_slice/mod.rs b/crates/bevy_sprite/src/texture_slice/mod.rs new file mode 100644 index 0000000000000..d16e6654ec4d0 --- /dev/null +++ b/crates/bevy_sprite/src/texture_slice/mod.rs @@ -0,0 +1,86 @@ +mod border_rect; +mod computed_slices; +mod slicer; + +use bevy_math::{Rect, Vec2}; +pub use border_rect::BorderRect; +pub use slicer::{SliceScaleMode, TextureSlicer}; + +pub(crate) use computed_slices::{ + compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices, +}; + +#[derive(Debug, Clone)] +pub(crate) struct TextureSlice { + /// texture area to draw + pub texture_rect: Rect, + /// slice draw size + pub draw_size: Vec2, + /// offset of the slice + pub offset: Vec2, +} + +impl TextureSlice { + /// Transforms the given slice in an collection of tiled subdivisions. + /// + /// # Arguments + /// + /// * `stretch_value` - The slice will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* (rect) are above `stretch_value`. + /// - `tile_x` - should the slice be tiled horizontally + /// - `tile_y` - should the slice be tiled vertically + #[must_use] + pub fn tiled(self, stretch_value: f32, (tile_x, tile_y): (bool, bool)) -> Vec { + if !tile_x && !tile_y { + return vec![self]; + } + let stretch_value = stretch_value.max(0.001); + let rect_size = self.texture_rect.size(); + // Each tile expected size + let expected_size = Vec2::new( + if tile_x { + rect_size.x * stretch_value + } else { + self.draw_size.x + }, + if tile_y { + rect_size.y * stretch_value + } else { + self.draw_size.y + }, + ); + let mut slices = Vec::new(); + let base_offset = Vec2::new( + -self.draw_size.x / 2.0, + self.draw_size.y / 2.0, // Start from top + ); + let mut offset = base_offset; + + let mut remaining_columns = self.draw_size.y; + while remaining_columns > 0.0 { + let size_y = expected_size.y.min(remaining_columns); + offset.x = base_offset.x; + offset.y -= size_y / 2.0; + let mut remaining_rows = self.draw_size.x; + while remaining_rows > 0.0 { + let size_x = expected_size.x.min(remaining_rows); + offset.x += size_x / 2.0; + let draw_size = Vec2::new(size_x, size_y); + let delta = draw_size / expected_size; + slices.push(Self { + texture_rect: Rect { + min: self.texture_rect.min, + max: self.texture_rect.min + self.texture_rect.size() * delta, + }, + draw_size, + offset: self.offset + offset, + }); + offset.x += size_x / 2.0; + remaining_rows -= size_x; + } + offset.y -= size_y / 2.0; + remaining_columns -= size_y; + } + slices + } +} diff --git a/crates/bevy_sprite/src/texture_slice/slicer.rs b/crates/bevy_sprite/src/texture_slice/slicer.rs new file mode 100644 index 0000000000000..b302d2e3562cf --- /dev/null +++ b/crates/bevy_sprite/src/texture_slice/slicer.rs @@ -0,0 +1,267 @@ +use super::{BorderRect, TextureSlice}; +use bevy_math::{vec2, Rect, Vec2}; +use bevy_reflect::Reflect; + +/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes +/// without needing to prepare multiple assets. The associated texture will be split into nine portions, +/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion. +/// +/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other +/// sections will be scaled or tiled. +/// +/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures. +#[derive(Debug, Clone, Reflect)] +pub struct TextureSlicer { + /// The sprite borders, defining the 9 sections of the image + pub border: BorderRect, + /// Defines how the center part of the 9 slices will scale + pub center_scale_mode: SliceScaleMode, + /// Defines how the 4 side parts of the 9 slices will scale + pub sides_scale_mode: SliceScaleMode, + /// Defines the maximum scale of the 4 corner slices (default to `1.0`) + pub max_corner_scale: f32, +} + +/// Defines how a texture slice scales when resized +#[derive(Debug, Copy, Clone, Default, Reflect)] +pub enum SliceScaleMode { + /// The slice will be stretched to fit the area + #[default] + Stretch, + /// The slice will be tiled to fit the area + Tile { + /// The slice will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above `stretch_value`. + /// + /// Example: `1.0` means that a 10 pixel wide image would repeat after 10 screen pixels. + /// `2.0` means it would repeat after 20 screen pixels. + /// + /// Note: The value should be inferior or equal to `1.0` to avoid quality loss. + /// + /// Note: the value will be clamped to `0.001` if lower + stretch_value: f32, + }, +} + +impl TextureSlicer { + /// Computes the 4 corner slices + #[must_use] + fn corner_slices(&self, base_rect: Rect, render_size: Vec2) -> [TextureSlice; 4] { + let coef = render_size / base_rect.size(); + let BorderRect { + left, + right, + top, + bottom, + } = self.border; + let min_coef = coef.x.min(coef.y).min(self.max_corner_scale); + [ + // Top Left Corner + TextureSlice { + texture_rect: Rect { + min: base_rect.min, + max: base_rect.min + vec2(left, top), + }, + draw_size: vec2(left, top) * min_coef, + offset: vec2( + -render_size.x + left * min_coef, + render_size.y - top * min_coef, + ) / 2.0, + }, + // Top Right Corner + TextureSlice { + texture_rect: Rect { + min: vec2(base_rect.max.x - right, base_rect.min.y), + max: vec2(base_rect.max.x, top), + }, + draw_size: vec2(right, top) * min_coef, + offset: vec2( + render_size.x - right * min_coef, + render_size.y - top * min_coef, + ) / 2.0, + }, + // Bottom Left + TextureSlice { + texture_rect: Rect { + min: vec2(base_rect.min.x, base_rect.max.y - bottom), + max: vec2(base_rect.min.x + left, base_rect.max.y), + }, + draw_size: vec2(left, bottom) * min_coef, + offset: vec2( + -render_size.x + left * min_coef, + -render_size.y + bottom * min_coef, + ) / 2.0, + }, + // Bottom Right Corner + TextureSlice { + texture_rect: Rect { + min: vec2(base_rect.max.x - right, base_rect.max.y - bottom), + max: base_rect.max, + }, + draw_size: vec2(right, bottom) * min_coef, + offset: vec2( + render_size.x - right * min_coef, + -render_size.y + bottom * min_coef, + ) / 2.0, + }, + ] + } + + /// Computes the 2 horizontal side slices (left and right borders) + #[must_use] + fn horizontal_side_slices( + &self, + [tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4], + base_rect: Rect, + render_size: Vec2, + ) -> [TextureSlice; 2] { + [ + // left + TextureSlice { + texture_rect: Rect { + min: base_rect.min + vec2(0.0, self.border.top), + max: vec2( + base_rect.min.x + self.border.left, + base_rect.max.y - self.border.bottom, + ), + }, + draw_size: vec2( + bl_corner.draw_size.x, + render_size.y - bl_corner.draw_size.y - tl_corner.draw_size.y, + ), + offset: vec2(-render_size.x + bl_corner.draw_size.x, 0.0) / 2.0, + }, + // right + TextureSlice { + texture_rect: Rect { + min: vec2( + base_rect.max.x - self.border.right, + base_rect.min.y + self.border.bottom, + ), + max: vec2(base_rect.max.x, base_rect.max.y - self.border.top), + }, + draw_size: vec2( + br_corner.draw_size.x, + render_size.y - (br_corner.draw_size.y + tr_corner.draw_size.y), + ), + offset: vec2(render_size.x - br_corner.draw_size.x, 0.0) / 2.0, + }, + ] + } + + /// Computes the 2 vertical side slices (bottom and top borders) + #[must_use] + fn vertical_side_slices( + &self, + [tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4], + base_rect: Rect, + render_size: Vec2, + ) -> [TextureSlice; 2] { + [ + // Bottom + TextureSlice { + texture_rect: Rect { + min: vec2( + base_rect.min.x + self.border.left, + base_rect.max.y - self.border.bottom, + ), + max: vec2(base_rect.max.x - self.border.right, base_rect.max.y), + }, + draw_size: vec2( + render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x), + bl_corner.draw_size.y, + ), + offset: vec2(0.0, bl_corner.offset.y), + }, + // Top + TextureSlice { + texture_rect: Rect { + min: base_rect.min + vec2(self.border.left, 0.0), + max: vec2( + base_rect.max.x - self.border.right, + base_rect.min.y + self.border.top, + ), + }, + draw_size: vec2( + render_size.x - (tl_corner.draw_size.x + tr_corner.draw_size.x), + tl_corner.draw_size.y, + ), + offset: vec2(0.0, tl_corner.offset.y), + }, + ] + } + + /// Slices the given `rect` into at least 9 sections. If the center and/or side parts are set to tile, + /// a bigger number of sections will be computed. + /// + /// # Arguments + /// + /// * `rect` - The section of the texture to slice in 9 parts + /// * `render_size` - The optional draw size of the texture. If not set the `rect` size will be used. + #[must_use] + pub(crate) fn compute_slices( + &self, + rect: Rect, + render_size: Option, + ) -> Vec { + let render_size = render_size.unwrap_or_else(|| rect.size()); + let mut slices = Vec::with_capacity(9); + // Corners + let corners = self.corner_slices(rect, render_size); + // Sides + let vertical_sides = self.vertical_side_slices(&corners, rect, render_size); + let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size); + // Center + let center = TextureSlice { + texture_rect: Rect { + min: rect.min + vec2(self.border.left, self.border.bottom), + max: vec2(rect.max.x - self.border.right, rect.max.y - self.border.top), + }, + draw_size: vec2( + render_size.x - (corners[2].draw_size.x + corners[3].draw_size.x), + render_size.y - (corners[2].draw_size.y + corners[0].draw_size.y), + ), + offset: Vec2::ZERO, + }; + + slices.extend(corners); + match self.center_scale_mode { + SliceScaleMode::Stretch => { + slices.push(center); + } + SliceScaleMode::Tile { stretch_value } => { + slices.extend(center.tiled(stretch_value, (true, true))); + } + } + match self.sides_scale_mode { + SliceScaleMode::Stretch => { + slices.extend(horizontal_sides); + slices.extend(vertical_sides); + } + SliceScaleMode::Tile { stretch_value } => { + slices.extend( + horizontal_sides + .into_iter() + .flat_map(|s| s.tiled(stretch_value, (false, true))), + ); + slices.extend( + vertical_sides + .into_iter() + .flat_map(|s| s.tiled(stretch_value, (true, false))), + ); + } + } + slices + } +} + +impl Default for TextureSlicer { + fn default() -> Self { + Self { + border: Default::default(), + center_scale_mode: Default::default(), + sides_scale_mode: Default::default(), + max_corner_scale: 1.0, + } + } +} diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index fb36d711f3d9a..1f9f707b0c211 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -14,7 +14,7 @@ multi-threaded = [] [dependencies] futures-lite = "2.0.1" async-executor = "1.7.2" -async-channel = "1.4.2" +async-channel = "2.1.0" async-io = { version = "2.0.0", optional = true } async-task = "4.2.0" concurrent-queue = "2.0.0" @@ -23,7 +23,7 @@ concurrent-queue = "2.0.0" wasm-bindgen-futures = "0.4" [dev-dependencies] -instant = { version = "0.1", features = ["wasm-bindgen"] } +web-time = { version = "0.2" } [lints] workspace = true diff --git a/crates/bevy_tasks/examples/busy_behavior.rs b/crates/bevy_tasks/examples/busy_behavior.rs index 8a74034e0ca90..23a43de017a59 100644 --- a/crates/bevy_tasks/examples/busy_behavior.rs +++ b/crates/bevy_tasks/examples/busy_behavior.rs @@ -1,4 +1,5 @@ use bevy_tasks::TaskPoolBuilder; +use web_time::{Duration, Instant}; // This sample demonstrates creating a thread pool with 4 tasks and spawning 40 tasks that spin // for 100ms. It's expected to take about a second to run (assuming the machine has >= 4 logical @@ -10,12 +11,12 @@ fn main() { .num_threads(4) .build(); - let t0 = instant::Instant::now(); + let t0 = Instant::now(); pool.scope(|s| { for i in 0..40 { s.spawn(async move { - let now = instant::Instant::now(); - while instant::Instant::now() - now < instant::Duration::from_millis(100) { + let now = Instant::now(); + while Instant::now() - now < Duration::from_millis(100) { // spin, simulating work being done } @@ -28,6 +29,6 @@ fn main() { } }); - let t1 = instant::Instant::now(); + let t1 = Instant::now(); println!("all tasks finished in {} secs", (t1 - t0).as_secs_f32()); } diff --git a/crates/bevy_tasks/examples/idle_behavior.rs b/crates/bevy_tasks/examples/idle_behavior.rs index daa2eaf2e2a89..3ce121f989fc3 100644 --- a/crates/bevy_tasks/examples/idle_behavior.rs +++ b/crates/bevy_tasks/examples/idle_behavior.rs @@ -1,4 +1,5 @@ use bevy_tasks::TaskPoolBuilder; +use web_time::{Duration, Instant}; // This sample demonstrates a thread pool with one thread per logical core and only one task // spinning. Other than the one thread, the system should remain idle, demonstrating good behavior @@ -13,8 +14,8 @@ fn main() { for i in 0..1 { s.spawn(async move { println!("Blocking for 10 seconds"); - let now = instant::Instant::now(); - while instant::Instant::now() - now < instant::Duration::from_millis(10000) { + let now = Instant::now(); + while Instant::now() - now < Duration::from_millis(10000) { // spin, simulating work being done } diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 81c26a1bc4c1b..1be0b0da174e3 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -171,7 +171,7 @@ impl TaskPool { /// the local executor on the main thread as it needs to share time with /// other things. /// - /// ```rust + /// ``` /// use bevy_tasks::TaskPool; /// /// TaskPool::new().with_local_executor(|local_executor| { diff --git a/crates/bevy_tasks/src/slice.rs b/crates/bevy_tasks/src/slice.rs index 4b5d875ea989b..8410478322ee0 100644 --- a/crates/bevy_tasks/src/slice.rs +++ b/crates/bevy_tasks/src/slice.rs @@ -10,7 +10,7 @@ pub trait ParallelSlice: AsRef<[T]> { /// /// # Example /// - /// ```rust + /// ``` /// # use bevy_tasks::prelude::*; /// # use bevy_tasks::TaskPool; /// let task_pool = TaskPool::new(); @@ -54,7 +54,7 @@ pub trait ParallelSlice: AsRef<[T]> { /// /// # Example /// - /// ```rust + /// ``` /// # use bevy_tasks::prelude::*; /// # use bevy_tasks::TaskPool; /// let task_pool = TaskPool::new(); @@ -104,7 +104,7 @@ pub trait ParallelSliceMut: AsMut<[T]> { /// /// # Example /// - /// ```rust + /// ``` /// # use bevy_tasks::prelude::*; /// # use bevy_tasks::TaskPool; /// let task_pool = TaskPool::new(); @@ -151,7 +151,7 @@ pub trait ParallelSliceMut: AsMut<[T]> { /// /// # Example /// - /// ```rust + /// ``` /// # use bevy_tasks::prelude::*; /// # use bevy_tasks::TaskPool; /// let task_pool = TaskPool::new(); diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 6b8bcdcf7d5d5..f34b98c91d213 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -360,7 +360,8 @@ impl TaskPool { unsafe { mem::transmute(external_executor) }; // SAFETY: As above, all futures must complete in this function so we can change the lifetime let scope_executor: &'env ThreadExecutor<'env> = unsafe { mem::transmute(scope_executor) }; - let spawned: ConcurrentQueue> = ConcurrentQueue::unbounded(); + let spawned: ConcurrentQueue>>> = + ConcurrentQueue::unbounded(); // shadow the variable so that the owned value cannot be used for the rest of the function // SAFETY: As above, all futures must complete in this function so we can change the lifetime let spawned: &'env ConcurrentQueue< @@ -560,7 +561,7 @@ impl TaskPool { /// the local executor on the main thread as it needs to share time with /// other things. /// - /// ```rust + /// ``` /// use bevy_tasks::TaskPool; /// /// TaskPool::new().with_local_executor(|local_executor| { diff --git a/crates/bevy_tasks/src/thread_executor.rs b/crates/bevy_tasks/src/thread_executor.rs index 81f51b689c7c0..dc989f902c12d 100644 --- a/crates/bevy_tasks/src/thread_executor.rs +++ b/crates/bevy_tasks/src/thread_executor.rs @@ -10,7 +10,7 @@ use futures_lite::Future; /// can spawn `Send` tasks from other threads. /// /// # Example -/// ```rust +/// ``` /// # use std::sync::{Arc, atomic::{AtomicI32, Ordering}}; /// use bevy_tasks::ThreadExecutor; /// diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 1b91430932e3b..eba165395e200 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -2,6 +2,7 @@ use ab_glyph::{FontArc, FontVec, InvalidFont, OutlinedGlyph}; use bevy_asset::Asset; use bevy_reflect::TypePath; use bevy_render::{ + render_asset::RenderAssetPersistencePolicy, render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, }; @@ -44,6 +45,7 @@ impl Font { .flat_map(|a| vec![255, 255, 255, (*a * 255.0) as u8]) .collect::>(), TextureFormat::Rgba8UnormSrgb, + RenderAssetPersistencePolicy::Unload, ) } } diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index cdf9e095889ba..78d81dfd322a4 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -2,10 +2,11 @@ use ab_glyph::{GlyphId, Point}; use bevy_asset::{Assets, Handle}; use bevy_math::Vec2; use bevy_render::{ + render_asset::RenderAssetPersistencePolicy, render_resource::{Extent3d, TextureDimension, TextureFormat}, texture::Image, }; -use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlas}; +use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlasLayout}; use bevy_utils::HashMap; #[cfg(feature = "subpixel_glyph_atlas")] @@ -42,16 +43,17 @@ impl From for SubpixelOffset { pub struct FontAtlas { pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, pub glyph_to_atlas_index: HashMap<(GlyphId, SubpixelOffset), usize>, - pub texture_atlas: Handle, + pub texture_atlas: Handle, + pub texture: Handle, } impl FontAtlas { pub fn new( textures: &mut Assets, - texture_atlases: &mut Assets, + texture_atlases: &mut Assets, size: Vec2, ) -> FontAtlas { - let atlas_texture = textures.add(Image::new_fill( + let texture = textures.add(Image::new_fill( Extent3d { width: size.x as u32, height: size.y as u32, @@ -60,12 +62,15 @@ impl FontAtlas { TextureDimension::D2, &[0, 0, 0, 0], TextureFormat::Rgba8UnormSrgb, + // Need to keep this image CPU persistent in order to add additional glyphs later on + RenderAssetPersistencePolicy::Keep, )); - let texture_atlas = TextureAtlas::new_empty(atlas_texture, size); + let texture_atlas = TextureAtlasLayout::new_empty(size); Self { texture_atlas: texture_atlases.add(texture_atlas), glyph_to_atlas_index: HashMap::default(), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 0), + texture, } } @@ -87,16 +92,18 @@ impl FontAtlas { pub fn add_glyph( &mut self, textures: &mut Assets, - texture_atlases: &mut Assets, + texture_atlases: &mut Assets, glyph_id: GlyphId, subpixel_offset: SubpixelOffset, texture: &Image, ) -> bool { let texture_atlas = texture_atlases.get_mut(&self.texture_atlas).unwrap(); - if let Some(index) = - self.dynamic_texture_atlas_builder - .add_texture(texture_atlas, textures, texture) - { + if let Some(index) = self.dynamic_texture_atlas_builder.add_texture( + texture_atlas, + textures, + texture, + &self.texture, + ) { self.glyph_to_atlas_index .insert((glyph_id, subpixel_offset), index); true diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 451db26bd50a1..bef50ac5ad0a7 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -6,7 +6,7 @@ use bevy_ecs::prelude::*; use bevy_math::Vec2; use bevy_reflect::Reflect; use bevy_render::texture::Image; -use bevy_sprite::TextureAtlas; +use bevy_sprite::TextureAtlasLayout; use bevy_utils::FloatOrd; use bevy_utils::HashMap; @@ -43,7 +43,8 @@ pub struct FontAtlasSet { #[derive(Debug, Clone, Reflect)] pub struct GlyphAtlasInfo { - pub texture_atlas: Handle, + pub texture_atlas: Handle, + pub texture: Handle, pub glyph_index: usize, } @@ -72,7 +73,7 @@ impl FontAtlasSet { pub fn add_glyph_to_atlas( &mut self, - texture_atlases: &mut Assets, + texture_atlases: &mut Assets, textures: &mut Assets, outlined_glyph: OutlinedGlyph, ) -> Result { @@ -145,10 +146,17 @@ impl FontAtlasSet { .find_map(|atlas| { atlas .get_glyph_index(glyph_id, position.into()) - .map(|glyph_index| (glyph_index, atlas.texture_atlas.clone_weak())) + .map(|glyph_index| { + ( + glyph_index, + atlas.texture_atlas.clone_weak(), + atlas.texture.clone_weak(), + ) + }) }) - .map(|(glyph_index, texture_atlas)| GlyphAtlasInfo { + .map(|(glyph_index, texture_atlas, texture)| GlyphAtlasInfo { texture_atlas, + texture, glyph_index, }) }) diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index b1765c0f038d0..f49211d610838 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -3,7 +3,7 @@ use bevy_asset::{AssetId, Assets}; use bevy_math::{Rect, Vec2}; use bevy_reflect::Reflect; use bevy_render::texture::Image; -use bevy_sprite::TextureAtlas; +use bevy_sprite::TextureAtlasLayout; use bevy_utils::tracing::warn; use glyph_brush_layout::{ BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, @@ -60,7 +60,7 @@ impl GlyphBrush { sections: &[SectionText], font_atlas_sets: &mut FontAtlasSets, fonts: &Assets, - texture_atlases: &mut Assets, + texture_atlases: &mut Assets, textures: &mut Assets, text_settings: &TextSettings, font_atlas_warning: &mut FontAtlasWarning, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 06783e12242fb..d0b5752266f55 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -94,6 +94,7 @@ impl Plugin for TextPlugin { PostUpdate, ( update_text2d_layout + .after(font_atlas_set::remove_dropped_font_atlas_sets) // Potential conflict: `Assets` // In practice, they run independently since `bevy_render::camera_update_system` // will only ever observe its own render target, and `update_text2d_layout` diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 90fb69c737273..56c861016cd23 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -12,7 +12,7 @@ use bevy_math::Vec2; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::texture::Image; -use bevy_sprite::TextureAtlas; +use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText, ToSectionText}; @@ -51,7 +51,7 @@ impl TextPipeline { linebreak_behavior: BreakLineOn, bounds: Vec2, font_atlas_sets: &mut FontAtlasSets, - texture_atlases: &mut Assets, + texture_atlases: &mut Assets, textures: &mut Assets, text_settings: &TextSettings, font_atlas_warning: &mut FontAtlasWarning, diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5c380aae0e001..6e6ae8ae00856 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -21,7 +21,7 @@ use bevy_render::{ view::{InheritedVisibility, ViewVisibility, Visibility}, Extract, }; -use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas}; +use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlasLayout}; use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; @@ -83,7 +83,7 @@ pub struct Text2dBundle { pub fn extract_text2d_sprite( mut commands: Commands, mut extracted_sprites: ResMut, - texture_atlases: Extract>>, + texture_atlases: Extract>>, windows: Extract>>, text2d_query: Extract< Query<( @@ -138,7 +138,7 @@ pub fn extract_text2d_sprite( color, rect: Some(atlas.textures[atlas_info.glyph_index]), custom_size: None, - image_handle_id: atlas.texture.id(), + image_handle_id: atlas_info.texture.id(), flip_x: false, flip_y: false, anchor: Anchor::Center.as_vec(), @@ -166,7 +166,7 @@ pub fn update_text2d_layout( mut font_atlas_warning: ResMut, windows: Query<&Window, With>, mut scale_factor_changed: EventReader, - mut texture_atlases: ResMut>, + mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, mut text_query: Query<(Entity, Ref, Ref, &mut TextLayoutInfo)>, diff --git a/crates/bevy_time/src/common_conditions.rs b/crates/bevy_time/src/common_conditions.rs index 1e4fbc8d5a16a..a175e4632acb4 100644 --- a/crates/bevy_time/src/common_conditions.rs +++ b/crates/bevy_time/src/common_conditions.rs @@ -1,11 +1,11 @@ -use crate::{Real, Time, Timer, TimerMode}; +use crate::{Real, Time, Timer, TimerMode, Virtual}; use bevy_ecs::system::Res; use bevy_utils::Duration; /// Run condition that is active on a regular time interval, using [`Time`] to advance /// the timer. The timer ticks at the rate of [`Time::relative_speed`]. /// -/// ```rust,no_run +/// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, Update}; /// # use bevy_ecs::schedule::IntoSystemConfigs; /// # use bevy_utils::Duration; @@ -13,8 +13,11 @@ use bevy_utils::Duration; /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins) -/// .add_systems(Update, tick.run_if(on_timer(Duration::from_secs(1)))) -/// .run(); +/// .add_systems( +/// Update, +/// tick.run_if(on_timer(Duration::from_secs(1))), +/// ) +/// .run(); /// } /// fn tick() { /// // ran once a second @@ -38,10 +41,11 @@ pub fn on_timer(duration: Duration) -> impl FnMut(Res