diff --git a/.cargo/config_fast_builds.toml b/.cargo/config_fast_builds.toml index 7270a7684b9957..ea6cacee2a1cd7 100644 --- a/.cargo/config_fast_builds.toml +++ b/.cargo/config_fast_builds.toml @@ -7,9 +7,9 @@ # # ## LLD # -# LLD is a linker from the LLVM project that supports Linux, Windows, MacOS, and WASM. It has the greatest +# LLD is a linker from the LLVM project that supports Linux, Windows, macOS, and Wasm. It has the greatest # platform support and the easiest installation process. It is enabled by default in this file for Linux -# and Windows. On MacOS, the default linker yields higher performance than LLD and is used instead. +# and Windows. On macOS, the default linker yields higher performance than LLD and is used instead. # # To install, please scroll to the corresponding table for your target (eg. `[target.x86_64-pc-windows-msvc]` # for Windows) and follow the steps under `LLD linker`. @@ -25,7 +25,7 @@ # your corresponding target, disable LLD by commenting out its `-Clink-arg=...` line, and enable Mold by # *uncommenting* its `-Clink-arg=...` line. # -# There is a fork of Mold named Sold that supports MacOS, but it is unmaintained and is about the same speed as +# There is a fork of Mold named Sold that supports macOS, but it is unmaintained and is about the same speed as # the default ld64 linker. For this reason, it is not included in this file. # # For more information, please see Mold's repository at . @@ -83,12 +83,21 @@ rustflags = [ # - Ubuntu: `sudo apt-get install mold clang` # - Fedora: `sudo dnf install mold clang` # - Arch: `sudo pacman -S mold clang` - # "-Clink-arg=-fuse-ld=/usr/bin/mold", + # "-Clink-arg=-fuse-ld=mold", # Nightly # "-Zshare-generics=y", # "-Zthreads=0", ] +# Some systems may experience linker performance issues when running doc tests. +# See https://github.com/bevyengine/bevy/issues/12207 for details. +rustdocflags = [ + # LLD linker + "-Clink-arg=-fuse-ld=lld", + + # Mold linker + # "-Clink-arg=-fuse-ld=mold", +] [target.x86_64-apple-darwin] rustflags = [ @@ -142,7 +151,7 @@ rustflags = [ ] # Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only' -# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains. +# In most cases the gains are negligible, but if you are on macOS and have slow compile times you should see significant gains. # [profile.dev] # debug = 1 diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index 179c766a94e139..00b904a514a6b5 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -12,7 +12,7 @@ # repository before you can use this action. # # This action will only install dependencies when the current operating system is Linux. It will do -# nothing on any other OS (MacOS, Windows). +# nothing on any other OS (macOS, Windows). name: Install Linux dependencies description: Installs the dependencies necessary to build Bevy on Linux. diff --git a/.github/contributing/engine_style_guide.md b/.github/contributing/engine_style_guide.md deleted file mode 100644 index cb13878743d35a..00000000000000 --- a/.github/contributing/engine_style_guide.md +++ /dev/null @@ -1,39 +0,0 @@ -# Style guide: Engine - -## Contributing - -For more advice on contributing to the engine, see the [relevant section](../../CONTRIBUTING.md#Contributing-code) of `CONTRIBUTING.md`. - -## General guidelines - -1. Prefer granular imports over glob imports like `bevy_ecs::prelude::*`. -2. Use a consistent comment style: - 1. `///` doc comments belong above `#[derive(Trait)]` invocations. - 2. `//` comments should generally go above the line in question, rather than in-line. - 3. Avoid `/* */` block comments, even when writing long comments. - 4. Use \`variable_name\` code blocks in comments to signify that you're referring to specific types and variables. - 5. Start comments with capital letters. End them with a period if they are sentence-like. -3. Use comments to organize long and complex stretches of code that can't sensibly be refactored into separate functions. -4. When using [Bevy error codes](https://bevyengine.org/learn/errors/) include a link to the relevant error on the Bevy website in the returned error message `... See: https://bevyengine.org/learn/errors/b0003`. - -## Rust API guidelines - -As a reference for our API development we are using the [Rust API guidelines][Rust API guidelines]. Generally, these should be followed, except for the following areas of disagreement: - -### Areas of disagreements - -Some areas mentioned in the [Rust API guidelines][Rust API guidelines] we do not agree with. These areas will be expanded whenever we find something else we do not agree with, so be sure to check these from time to time. - -> All items have a rustdoc example - -- This guideline is too strong and not applicable for everything inside of the Bevy game engine. For functionality that requires more context or needs a more interactive demonstration (such as rendering or input features), make use of the `examples` folder instead. - -> Examples use ?, not try!, not unwrap - -- This guideline is usually reasonable, but not always required. - -> Only smart pointers implement Deref and DerefMut - -- Generally a good rule of thumb, but we're probably going to deliberately violate this for single-element wrapper types like `Life(u32)`. The behavior is still predictable and it significantly improves ergonomics / new user comprehension. - -[Rust API guidelines]: https://rust-lang.github.io/api-guidelines/about.html diff --git a/.github/contributing/example_style_guide.md b/.github/contributing/example_style_guide.md deleted file mode 100644 index 1287728ec22e30..00000000000000 --- a/.github/contributing/example_style_guide.md +++ /dev/null @@ -1,64 +0,0 @@ -# Style guide: Examples - -For more advice on writing examples, see the [relevant section](../../CONTRIBUTING.md#writing-examples) of CONTRIBUTING.md. - -## Organization - -1. Examples should live in an appropriate subfolder of `/examples`. -2. Examples should be a single file if possible. -3. Assets live in `./assets`. Try to avoid adding new assets unless strictly necessary to keep the repo small. Don't add "large" asset files. -4. Each example should try to follow this order: - 1. Imports - 2. A `fn main()` block - 3. Example logic -5. Try to structure app / plugin construction in the same fashion as the actual code. -6. Examples should typically not have tests, as they are not directly reusable by the Bevy user. - -## Stylistic preferences - -1. Use simple, descriptive variable names. - 1. Avoid names like `MyComponent` in favor of more descriptive terms like `Events`. - 2. Prefer single letter differentiators like `EventsA` and `EventsB` to nonsense words like `EventsFoo` and `EventsBar`. - 3. Avoid repeating the type of variables in their name where possible. For example, `Color` should be preferred to `ColorComponent`. -2. Prefer glob imports of `bevy::prelude::*` and `bevy::sub_crate::*` over granular imports (for terseness). -3. Use a consistent comment style: - 1. `///` doc comments belong above `#[derive(Trait)]` invocations. - 2. `//` comments should generally go above the line in question, rather than in-line. - 3. Avoid `/* */` block comments, even when writing long comments. - 4. Use \`variable_name\` code blocks in comments to signify that you're referring to specific types and variables. - 5. Start comments with capital letters; end them with a period if they are sentence-like. -4. Use comments to organize long and complex stretches of code that can't sensibly be refactored into separate functions. -5. Avoid making variables `pub` unless it is needed for your example. - -## Code conventions - -1. Refactor configurable values ("magic numbers") out into constants with clear names. -2. Prefer `for` loops over `.for_each`. The latter is faster (for now), but it is less clear for beginners, less idiomatic, and less flexible. -3. Use `.single` and `.single_mut` where appropriate. -4. In Queries, prefer `With` filters over actually fetching unused data with `&T`. -5. Prefer disjoint queries using `With` and `Without` over param sets when you need more than one query in a single system. -6. Prefer structs with named fields over tuple structs except in the case of single-field wrapper types. -7. Use enum-labels over string-labels for app / schedule / etc. labels. - -## "Feature" examples - -These examples demonstrate the usage of specific engine features in clear, minimal ways. - -1. Focus on demonstrating exactly one feature in an example -2. Try to keep your names divorced from the context of a specific game, and focused on the feature you are demonstrating. -3. Where they exist, show good alternative approaches to accomplish the same task and explain why you may prefer one over the other. -4. Examples should have a visible effect when run, either in the command line or a graphical window. - -## "Game" examples - -These examples show how to build simple games in Bevy in a cohesive way. - -1. Each of these examples lives in the [/examples/games] folder. -2. Aim for minimum but viable status: the game should be playable and not obviously buggy but does not need to be polished, featureful, or terribly fun. -3. Focus on code quality and demonstrating good, extensible patterns for users. - 1. Make good use of enums and states to organize your game logic. - 2. Keep components as small as possible but no smaller: all of the data on a component should generally be accessed at once. - 3. Keep systems small: they should have a clear single purpose. - 4. Avoid duplicating logic across similar entities whenever possible by sharing systems and components. -4. Use `///` doc comments to explain what each function / struct does as if the example were part of a polished production codebase. -5. Arrange your code into modules within the same file to allow for simple code folding / organization. diff --git a/.github/example-run/ambiguity_detection.ron b/.github/example-run/ambiguity_detection.ron new file mode 100644 index 00000000000000..72873dd6677d59 --- /dev/null +++ b/.github/example-run/ambiguity_detection.ron @@ -0,0 +1,2 @@ +( +) diff --git a/.github/start-wasm-example/tests/wasm_example.spec.ts b/.github/start-wasm-example/tests/wasm_example.spec.ts index 6d7a6676d16172..9165d00f98cc06 100644 --- a/.github/start-wasm-example/tests/wasm_example.spec.ts +++ b/.github/start-wasm-example/tests/wasm_example.spec.ts @@ -6,7 +6,7 @@ test.beforeEach(async ({ page }) => { const MAX_TIMEOUT_FOR_TEST = 300_000; -test.describe('WASM example', () => { +test.describe('Wasm example', () => { test('Wait for success', async ({ page }, testInfo) => { let start = new Date().getTime(); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8520f5f0160625..a73d77cc838c85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: run: cargo run -p ci -- lints miri: - # Explicity use MacOS 14 to take advantage of M1 chip. + # Explicity use macOS 14 to take advantage of M1 chip. runs-on: macos-14 timeout-minutes: 60 steps: @@ -219,7 +219,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.23.1 + uses: crate-ci/typos@v1.23.6 - name: Typos info if: failure() run: | @@ -231,7 +231,7 @@ jobs: run-examples-macos-metal: - # Explicity use MacOS 14 to take advantage of M1 chip. + # Explicity use macOS 14 to take advantage of M1 chip. runs-on: macos-14 timeout-minutes: 30 steps: @@ -436,28 +436,3 @@ jobs: echo " Example: 'use bevy::sprite::MaterialMesh2dBundle;' instead of 'bevy_internal::sprite::MaterialMesh2dBundle;'" exit 1 fi - check-cfg: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-check-doc-${{ hashFiles('**/Cargo.toml') }} - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} - - name: Install Linux dependencies - uses: ./.github/actions/install-linux-deps - with: - wayland: true - xkb: true - - name: Build and check cfg typos - # See tools/ci/src/main.rs for the commands this runs - run: cargo run -p ci -- cfg-check diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 49e6b2cd497c5e..17ac22019ebf01 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -58,8 +58,22 @@ jobs: - name: Build docs env: # needs to be in sync with [package.metadata.docs.rs] - RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs - run: cargo doc --all-features --no-deps -p bevy -Zunstable-options -Zrustdoc-scrape-examples + RUSTFLAGS: --cfg docsrs_dep + RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs --generate-link-to-definition + run: | + cargo doc \ + -Zunstable-options \ + -Zrustdoc-scrape-examples \ + --all-features \ + --workspace \ + --no-deps \ + --document-private-items \ + --exclude ci \ + --exclude errors \ + --exclude bevy_mobile_example \ + --exclude build-wasm-example \ + --exclude build-templated-pages \ + --exclude example-showcase # This adds the following: # - A top level redirect to the bevy crate documentation @@ -69,7 +83,7 @@ jobs: run: | echo "" > target/doc/index.html echo "dev-docs.bevyengine.org" > target/doc/CNAME - echo "User-Agent: *\nDisallow: /" > target/doc/robots.txt + echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt rm target/doc/.lock - name: Upload site artifact diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index 778d8a27a26557..f5fab3f976d6d3 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -209,7 +209,7 @@ jobs: npx playwright install --with-deps cd ../.. - - name: First WASM build + - name: First Wasm build run: | cargo build --release --example ui --target wasm32-unknown-unknown diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index c3c67fab53a486..23bcbcf8be9389 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -41,5 +41,5 @@ jobs: repo: context.repo.repo, body: `**Welcome**, new contributor! - Please make sure you've read our [contributing guide](https://github.com/bevyengine/bevy/blob/main/CONTRIBUTING.md) and we look forward to reviewing your pull request shortly ✨` + Please make sure you've read our [contributing guide](https://bevyengine.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` }) diff --git a/.gitignore b/.gitignore index db8ddeb9d0279a..2fab476252e7c9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ Cargo.lock /.idea /.vscode /benches/target +/tools/compile_fail_utils/target dxcompiler.dll dxil.dll diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 7be31baea9d6b2..00000000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,6946 +0,0 @@ - - - -# Changelog - -While we try to keep the `Unreleased` changes updated, it is often behind and does not include -all merged pull requests. To see a list of all changes since the latest release, you may compare -current changes on git with [previous release tags][git_tag_comparison]. - -[git_tag_comparison]: https://github.com/bevyengine/bevy/compare/v0.13.0...main - -## Version 0.13.0 (2024-02-17) - -### A-Rendering + A-Windowing - -- [Allow prepare_windows to run off main thread.][11660] -- [Allow prepare_windows to run off main thread on all platforms][11672] -- [don't run `create_surfaces` system if not needed][11720] -- [fix create_surfaces system ordering][11747] - -### A-Animation + A-Reflection - -- [Add type registrations for animation types][11889] - -### A-Assets - -- [Don't `.unwrap()` in `AssetPath::try_parse`][10452] -- [feat: `Debug` implemented for `AssetMode`][10494] -- [Remove rogue : from embedded_asset! docs][10516] -- [use `tree` syntax to explain bevy_rock file structure][10523] -- [Make AssetLoader/Saver Error type bounds compatible with anyhow::Error][10493] -- [Fix untyped labeled asset loading][10514] -- [Add `load_untyped` to LoadContext][10526] -- [fix example custom_asset_reader on wasm][10574] -- [`ReadAssetBytesError::Io` exposes failing path][10450] -- [Added Method to Allow Pipelined Asset Loading][10565] -- [Add missing asset load error logs for load_folder and load_untyped][10578] -- [Fix wasm builds with file_watcher enabled][10589] -- [Do not panic when failing to create assets folder (#10613)][10614] -- [Use handles for queued scenes in SceneSpawner][10619] -- [Fix file_watcher feature hanging indefinitely][10585] -- [derive asset for enums][10410] -- [Ensure consistency between Un/Typed `AssetId` and `Handle`][10628] -- [Fix Asset Loading Bug][10698] -- [remove double-hasing of typeid for handle][10699] -- [AssetMetaMode][10623] -- [Fix GLTF scene dependencies and make full scene renders predictable][10745] -- [Print precise and correct watch warnings (and only when necessary)][10787] -- [Allow removing and reloading assets with live handles][10785] -- [Add GltfLoaderSettings][10804] -- [Refactor `process_handle_drop_internal()` in bevy_asset][10920] -- [fix base64 padding when loading a gltf file][11053] -- [assets should be kept on CPU by default][11212] -- [Don't auto create assets folder][11218] -- [Use `impl Into` for `Assets::add`][10878] -- [Add `reserve_handle` to `Assets`.][10939] -- [Better error message on incorrect asset label][11254] -- [GLTF extension support][11138] -- [Fix embedded watcher to work with external crates][11370] -- [Added AssetLoadFailedEvent, UntypedAssetLoadFailedEvent][11369] -- [auto create imported asset folder if needed][11284] -- [Fix minor typo][11491] -- [Include asset path in get_meta_path panic message][11504] -- [Fix documentation for `AssetReader::is_directory` function][11538] -- [AssetSaver and AssetTransformer split][11260] -- [AssetPath source parse fix][11543] -- [Allow TextureAtlasBuilder in AssetLoader][11548] -- [Add a getter for asset watching status on `AssetServer`][11578] -- [Make SavedAsset::get_labeled accept &str as label][11612] -- [Added Support for Extension-less Assets][10153] -- [Fix embedded asset path manipulation][10383] -- [Fix AssetTransformer breaking LabeledAssets][11626] -- [Put asset_events behind a run condition][11800] -- [Use Asset Path Extension for `AssetLoader` Disambiguation][11644] - -### A-Core + A-App - -- [Add Accessibility plugin to default plugins docs][11512] - -### A-Accessibility - -- [Add html tags required for accessibility][10989] -- [missed negation during accessibility refactor][11206] - -### A-Transform - -- [Add `Transform::is_finite`][10592] - -### A-ECS + A-Hierarchy - -- [Add a doc note about despawn footgun][10889] - -### A-Text - -- [Rename `TextAlignment` to `JustifyText`.][10854] -- [Subtract 1 from text positions to account for glyph texture padding.][11662] - -### A-Assets + A-UI - -- [UI and unloaded assets: don't filter out nodes with an unloaded image][11205] - -### A-Utils + A-Time - -- [Make SystemTime available in both native and wasm][10980] - -### A-Rendering + A-Assets - -- [Fix shader import hot reloading on windows][10502] -- [Unload render assets from RAM][10520] -- [mipmap levels can be 0 and they should be interpreted as 1][11767] - -### A-Physics - -- [refactor collide code (Adopted)][11106] -- [Use `IntersectsVolume` for breakout example collisions][11500] - -### A-ECS + A-Editor + A-App + A-Diagnostics - -- [System Stepping implemented as Resource][8453] - -### A-Reflection + A-Scenes - -- [Implement and register Reflect (value) for CameraRenderGraph and CameraMainTextureUsages][11878] - -### A-Audio + A-Windowing - -- [Winit update: fix suspend on Android][11403] - -### A-Build-System + A-Meta - -- [Standardize toml format with taplo][10594] - -### A-ECS + A-Time - -- [Wait until `FixedUpdate` can see events before dropping them][10077] -- [Add First/Pre/Post/Last schedules to the Fixed timestep][10977] -- [Add run conditions for executing a system after a delay][11095] -- [Add paused run condition][11313] - -### A-Meta - -- [Add "update screenshots" to release checklist][10369] -- [Remove references to specific projects from the readme][10836] -- [Fix broken link between files][10962] -- [[doc] Fix typo in CONTRIBUTING.md][10971] -- [Remove unused namespace declarations][10965] -- [Add docs link to root `Cargo.toml`][10998] -- [Migrate third party plugins guidelines to the book][11242] -- [Run markdownlint][11386] -- [Improve `config_fast_builds.toml`][11529] -- [Use `-Z threads=0` option in `config_fast_builds.toml`][11541] -- [CONTRIBUTING.md: Mention splitting complex PRs][11703] - -### A-Time - -- [docs: use `read` instead of deprecated `iter`][10376] -- [Rename `Time::::overstep_percentage()` and `Time::::overstep_percentage_f64()`][10448] -- [Rename `Timer::{percent,percent_left}` to `Timer::{fraction,fraction_remaining}`][10442] -- [Document how to configure FixedUpdate][10564] -- [Add discard_overstep function to `Time`][10453] - -### A-Assets + A-Reflection - -- [Register `AssetPath` as type for reflection][11483] - -### A-Diagnostics + A-Utils - -- [move once from bevy_log to bevy_utils, to allow for it's use in bevy_ecs][11419] - -### A-Windowing + A-App - -- [Revert `App::run()` behavior/Remove `winit` specific code from `bevy_app`][10389] - -### A-ECS + A-Scenes - -- [Make the MapEntities trait generic over Mappers, and add a simpler EntityMapper][11428] - -### A-Hierarchy - -- [bevy_hierarchy: add some docs][10598] -- [Make bevy_app and reflect opt-out for bevy_hierarchy.][10721] -- [Add `bevy_hierarchy` Crate and plugin documentation][10951] -- [Rename "AddChild" to "PushChild"][11194] -- [Inline trivial methods in bevy_hierarchy][11332] - -### A-ECS + A-App - -- [Add custom schedule example][11527] - -### A-Transform + A-Math - -- [return Direction3d from Transform::up and friends][11604] - -### A-UI + A-Text - -- [Improved Text Rendering][10537] -- [Feature-gate all references to `bevy_text` in `bevy_ui`][11391] - -### A-Input - -- [Make ButtonSettings.is_pressed/released public][10534] -- [Rename `Input` to `ButtonInput`][10859] -- [Add method to check if all inputs are pressed][11010] -- [Add window entity to TouchInput events][11128] -- [Extend `Touches` with clear and reset methods][10930] -- [Add logical key data to KeyboardInput][11400] -- [Derive Ord for GamepadButtonType.][11791] -- [Add delta to CursorMoved event][11710] - -### A-Rendering + A-Diagnostics - -- [Use `warn_once` where relevant instead of manually implementing a single warn check][11693] - -### A-Rendering - -- [Fix bevy_pbr shader function name][10423] -- [Implement Clone for VisibilityBundle and SpatialBundle][10394] -- [Reexport `wgpu::Maintain`][10461] -- [Use a consistent scale factor and resolution in stress tests][10474] -- [Ignore inactive cameras][10543] -- [Add shader_material_2d example][10542] -- [More inactive camera checks][10555] -- [Fix post processing example to only run effect on camera with settings component][10560] -- [Make sure added image assets are checked in camera_system][10556] -- [Ensure ExtendedMaterial works with reflection (to enable bevy_egui_inspector integration)][10548] -- [Explicit color conversion methods][10321] -- [Re-export wgpu BufferAsyncError][10611] -- [Improve shader_material example][10547] -- [Non uniform transmission samples][10674] -- [Explain how `AmbientLight` is inserted and configured][10712] -- [Add wgpu_pass method to TrackedRenderPass][10722] -- [Add a `depth_bias` to `Material2d`][10683] -- [Use as_image_copy where possible][10733] -- [impl `From` for ClearColorConfig][10734] -- [Ensure instance_index push constant is always used in prepass.wgsl][10706] -- [Bind group layout entries][10224] -- [prepass vertex shader always outputs world position][10657] -- [Swap material and mesh bind groups][10485] -- [try_insert Aabbs][10801] -- [Fix prepass binding issues causing crashes when not all prepass bindings are used][10788] -- [Fix binding group in custom_material_2d.wgsl][10841] -- [Normalize only nonzero normals for mikktspace normal maps][10905] -- [light renderlayers][10742] -- [Explain how RegularPolygon mesh is generated][10927] -- [Fix Mesh2d normals on webgl][10967] -- [Update to wgpu 0.18][10266] -- [Fix typo in docs for `ViewVisibility`][10979] -- [Add docs to bevy_sprite a little][10947] -- [Fix BindingType import warning][10818] -- [Update texture_atlas example with different padding and sampling][10073] -- [Update AABB when Sprite component changes in calculate_bounds_2d()][11016] -- [OrthographicProjection.scaling_mode is not just for resize][11024] -- [Derive `Debug` for `BloomCompositeMode`][11041] -- [Document None conditions on compute_aabb][11051] -- [Replace calculation with function call][11077] -- [Register Camera types.][11069] -- [Add example for pixel-perfect grid snapping in 2D][8112] -- [Misc cleanup][11134] -- [Keep track of when a texture is first cleared][10325] -- [Fix Mesh::ATTRIBUTE_UV_0 documentation][11110] -- [Do not load prepass normals for transmissive materials][11140] -- [Export tonemapping_pipeline_key (2d), alpha_mode_pipeline_key][11166] -- [Simplify examples/3d/orthographic][11045] -- [Implement lightmaps.][10231] -- [Bump the vertex attribute index for prepass joints.][11191] -- [Fix: Gizmos crash due to the persistence policy being set to `Unload`. Change it to `Keep`][11192] -- [Usability methods for RenderTargets and image handles][10736] -- [Explain Camera physical size is in pixel][11189] -- [update Outdated comment][11243] -- [Revert "Implement minimal reflection probes. (#10057)"][11307] -- [Explain OrthographicProjection.scale][11023] -- [`Mul` for ScalingMode][11030] -- [Rustdoc examples for OrthographicProjection][11031] -- [Option to enable deterministic rendering][11248] -- [Fix ssao only sampling mip 0][11292] -- [Revert "Implement minimal reflection probes. (#10057)"][11307] -- [Sprite slicing and tiling][10588] -- [Approximate indirect specular occlusion][11152] -- [Texture Atlas rework][5103] -- [Exposure settings (adopted)][11347] -- [Remove Vec from GpuArrayBuffer][11368] -- [Make `DynamicUniformBuffer::push` accept an `&T` instead of `T`][11373] -- [Restore brightness in the remaining three examples after exposure PR][11389] -- [Customizable camera main texture usage][11412] -- [Cleanup deterministic example][11416] -- [Implement minimal reflection probes (fixed macOS, iOS, and Android).][11366] -- [optimize batch_and_prepare_render_phase][11323] -- [add `storage_texture` option to as_bind_group macro][9943] -- [Revert rendering-related associated type name changes][11027] -- [Meshlet prep][11442] -- [Reuse sampler when creating cached bind groups][10610] -- [Add Animated Material example][11524] -- [Update to wgpu 0.19 and raw-window-handle 0.6][11280] -- [Fix bug where Sprite::rect was ignored][11480] -- [Added documentation explaining the difference between lumens and luxes][11551] -- [Fix infinite asset preparation due to undrained AssetEvent events][11383] -- [Workaround for ICE in the DXC shader compiler in debug builds with an `EnvironmentMapLight`][11487] -- [Refactor tonemapping example's image viewer update into two systems][11519] -- [Add `Mesh` transformation][11454] -- [Fix specular envmap in deferred][11534] -- [Add `Meshable` trait and implement meshing for 2D primitives][11431] -- [Optimize extract_clusters and prepare_clusters systems][10633] -- [RenderAssetPersistencePolicy → RenderAssetUsages][11399] -- [RenderGraph Labelization][10644] -- [Gate diffuse and specular transmission behind shader defs][11627] -- [Add helpers for translate, rotate, and scale operations - Mesh][11675] -- [CameraProjection::compute_frustum][11139] -- [Added formats to `MeshVertexAttribute` constant's docstrings][11705] -- [Async pipeline compilation][10812] -- [sort by pipeline then mesh for non transparent passes for massively better batching][11671] -- [Added remove_indices to Mesh][11733] -- [Implement irradiance volumes.][10268] -- [Mesh insert indices][11745] -- [Don't try to create a uniform buffer for light probes if there are no views.][11751] -- [Properly check for result when getting pipeline in Msaa][11758] -- [wait for render app when main world is dropped][11737] -- [Deprecate shapes in `bevy_render::mesh::shape`][11773] -- [Cache the QueryState used to drop swapchain TextureViews][11781] -- [Multithreaded render command encoding][9172] -- [Fix `Quad` deprecation message mentioning a type that doesn't exist][11798] -- [Stop extracting mesh entities to the render world.][11803] -- [Stop copying the light probe array to the stack in the shader.][11805] -- [Add `Mesh::merge`][11456] -- [Call a TextureAtlasLayout a layout and not an atlas][11783] -- [fix shadow batching][11645] -- [Change light defaults & fix light examples][11581] -- [New Exposure and Lighting Defaults (and calibrate examples)][11868] -- [Change MeshUniform::new() to be public.][11880] -- [Rename Core Render Graph Labels][11882] -- [Support optional clear color in ColorAttachment.][11884] -- [irradiance: use textureSampleLevel for WebGPU support][11893] -- [Add configuration for async pipeline creation on RenderPlugin][11847] -- [Derive Reflect for Exposure][11907] -- [Add `MeshPipelineKey::LIGHTMAPPED` as applicable during the shadow map pass.][11910] -- [Irradiance volume example tweaks][11911] -- [Disable irradiance volumes on WebGL and WebGPU.][11909] -- [Remove `naga_oil` dependency from `bevy_pbr`][11914] - -### A-Scenes - -- [Re-export `ron` in `bevy_scene`][10529] -- [Fix load scene example to use proper serialization format for rotation field][10638] -- [Mention DynamicSceneBuilder in doc comment][10780] -- [Mention DynamicSceneBuilder in scene example][10441] -- [Implement Std traits for `SceneInstanceReady`][11003] -- [Change SceneSpawner::spawn_dynamic_sync to return InstanceID][11239] -- [Fix scene example][11289] -- [Send `SceneInstanceReady` only once per scene][11002] - -### A-Utils - -- [bevy_utils: Export `generate_composite_uuid` utility function][10496] -- [Save an instruction in `EntityHasher`][10648] -- [Add SystemTime to bevy_utils][11054] -- [Re-export smallvec crate from bevy_utils][11006] -- [Enable cloning EntityHashMap and PreHashMap][11178] -- [impl `Borrow` and `AsRef` for `CowArc`][11616] -- [Hash stability guarantees][11690] -- [Deprecating hashbrown reexports][11721] -- [Update ahash to 0.8.7][11785] - -### A-UI - -- [ui material: fix right border width][10421] -- [Add PartialEq to Anchor][10424] -- [UI Material: each material should have its own buffer][10422] -- [UI Materials: ignore entities with a `BackgroundColor` component][10434] -- [Fix panic when using image in UiMaterial][10591] -- [Make clipped areas of UI nodes non-interactive][10454] -- [Fix typo in resolve_outlines_system][10730] -- [Clip outlines by the node's own clipping rect, not the parent's.][10922] -- [Give UI nodes with `Display::None` an empty clipping rect][10942] -- [Create serialize feature for bevy_ui][11188] -- [Made the remaining types from bevy_ui to reflect the Default trait if…][11199] -- [Camera-driven UI][10559] -- [fix occasional crash moving ui root nodes][11371] -- [Fix panic on Text UI without Cameras][11405] -- [Allow user to choose default ui camera][11436] -- [Rustdoc links in bevy_ui][11555] -- [Avoid unconditionally unwrapping the Result - UI Stack System][11575] - -### A-Assets + A-Diagnostics - -- [Fix asset loader registration warning][11870] - -### A-Audio + A-Reflection - -- [Reflect and register audio-related types][10484] - -### A-Audio - -- [Add `VolumeLevel::ZERO`][10608] -- [Deduplicate systems in bevy_audio][10906] -- [Non-Intrusive refactor of `play_queued_audio_system()`][10910] -- [docs: AnimationPlayer::play doesn't have transition_duration arg][10970] -- [Remove the ability to ignore global volume][11092] -- [Optional override for global spatial scale][10419] - -### A-Tasks - -- [Make FakeTask public on singlethreaded context][10517] -- [Re-export `futures_lite` in `bevy_tasks`][10670] -- [bump bevy_tasks futures-lite to 2.0.1][10675] -- [Fix wrong transmuted type in `TaskPool::scope_with_executor_inner`][11455] -- [Use `std::thread::sleep` instead of spin-waiting in the async_compute example][11856] - -### A-ECS - -- [Use `EntityHashMap` for `EntityMapper`][10415] -- [Allow registering boxed systems][10378] -- [Remove unnecessary if statement in scheduler][10446] -- [Optimize `Entity::eq`][10519] -- [Add 'World::run_system_with_input' function + allow `World::run_system` to get system output][10380] -- [Update `Event` send methods to return `EventId`][10551] -- [Some docs for IntoSystemSet][10563] -- [Link to `In` in `pipe` documentation][10596] -- [Optimise `Entity` with repr align & manual `PartialOrd`/`Ord`][10558] -- [Allow #[derive(Bundle)] on tuple structs (take 3)][10561] -- [Add an `Entry` api to `EntityWorldMut`.][10650] -- [Make impl block for RemovedSystem generic][10651] -- [Append commands][10400] -- [Rustdoc example for Ref][10682] -- [Link to `Main` schedule docs from other schedules][10691] -- [Warn that Added/Changed filters do not see deferred changes][10681] -- [Fix non-functional nondeterministic_system_order example][10719] -- [Copy over docs for `Condition` trait from PR #10718][10748] -- [Implement `Drop` for `CommandQueue`][10746] -- [Split WorldQuery into WorldQueryData and WorldQueryFilter][9918] -- [Make IntoSystemConfigs::into_configs public API (visible in docs)][10624] -- [Override QueryIter::fold to port Query::for_each perf gains to select Iterator combinators][6773] -- [Deprecate QueryState::for_each_unchecked][10815] -- [Clarifying Commands' purpose][10837] -- [Make ComponentId typed in Components][10770] -- [Reduced `TableRow` `as` Casting][10811] -- [Add `EntityCommands.retain` and `EntityWorldMut.retain`][10873] -- [Remove unnecessary ResMut in examples][10879] -- [Add a couple assertions for system types][10893] -- [Remove reference to default schedule][10918] -- [Improve `EntityWorldMut.remove`, `retain` and `despawn` docs by linking to more detail][10943] -- [Reorder fields in SystemSchedule][10764] -- [Rename `WorldQueryData` & `WorldQueryFilter` to `QueryData` & `QueryFilter`][10779] -- [Fix soundness of `UnsafeWorldCell` usage example][10941] -- [Actually check alignment in BlobVec test aligned_zst][10885] -- [Rename `Q` type parameter to `D` when referring to `WorldQueryData`][10782] -- [Allow the editing of startup schedules][10969] -- [Auto insert sync points][9822] -- [Simplify lifetimes in `QueryState` methods][10937] -- [Add is_resource_changed_by_id + is_resource_added_by_id][11012] -- [Rename some lifetimes (ResMut etc) for clarity][11021] -- [Add non-existent entity behavior to Has doc][11025] -- [Fix typo in docs for Has][11028] -- [Add insert_state to App.][11043] -- [Explain Changed, Added are not archetype filters][11049] -- [Add missing colon in `States` documentation][11064] -- [Explain EventWriter limits concurrency][11063] -- [Better doc for SystemName][11084] -- [impl ExclusiveSystemParam for WorldId][11164] -- [impl ExclusiveSystemParam for PhantomData][11153] -- [Remove little warn on bevy_ecs][11149] -- [Rename `ArchetypeEntity::entity` into `ArchetypeEntity::id`][11118] -- [Fixed Typo in the description of EntityMut][11103] -- [Implement Deref and DerefMut for In][11104] -- [impl ExclusiveSystemParam for SystemName][11163] -- [Print a warning for un-applied commands being dropped from a CommandQueue][11146] -- [Implement TypePath for EntityHash][11195] -- [Fix integer overflow in BlobVec::push for ZST][10799] -- [Fix integer overflow in BlobVec::reserve_exact][11234] -- [StateTransitionEvent][11089] -- [Restore support for running `fn` `EntityCommands` on entities that might be despawned][11107] -- [Remove apply_deferred example][11142] -- [Minimize small allocations by dropping the tick Vecs from Resources][11226] -- [Change Entity::generation from u32 to NonZeroU32 for niche optimization][9907] -- [fix B0003 example and update logs][11162] -- [Unified identifer for entities & relations][9797] -- [Simplify conditions][11316] -- [Add example using `State` in docs][11319] -- [Skip rehashing TypeIds][11268] -- [Make `TypeId::hash` more robust in case of upstream rustc changes][11334] -- [Fix doc of [`Schedules`] to mention exclusion of current schedule.][11360] -- [Dynamic queries and builder API][9774] -- [Remove duplicate `#[automatically_derived]` in ECS macro][11388] -- [Get Change Tick methods for Resources][11404] -- [Optional state][11417] -- [Double the capacity when BlobVec is full][11167] -- [document which lifetime is needed for systemparam derive][11321] -- [refactor: Simplify lifetimes for `Commands` and related types][11445] -- [Implement `Debug` for `CommandQueue`][11444] -- [Fix typo in comment][11486] -- [Rename Schedule::name to Schedule::label][11531] -- [Exclusive systems can now be used for one-shot systems][11560] -- [added ability to get `Res` from `World` with `World::get_resource_ref`][11561] -- [bevy_ecs: Add doc example for par_iter_mut (#11311)][11499] -- [Add an example demonstrating how to send and receive events in the same system][11574] -- [Add a doctest example for EntityMapper][11583] -- [Rephrase comment about `Local` for clarity. (Adopted)][11129] -- [Use batch spawn in benchmarks][11611] -- [Fix bug where events are not being dropped][11528] -- [Make Archetypes.archetype_component_count private][10774] -- [Deprecated Various Component Methods from `Query` and `QueryState`][9920] -- [`System::type_id` Consistency][11728] -- [Typo in [`ScheduleLabel`] derive macro][11764] -- [Mention Resource where missing from component/resource related type docs][11769] -- [Expose query accesses][11700] -- [Add a method for detecting changes within a certain scope][11687] -- [Fix double indirection when applying command queues][11822] -- [Immediately poll the executor once before spawning it as a task][11801] -- [Fix small docs misformat in `BundleInfo::new`][11855] -- [`FilteredEntityRef` conversions][11838] - -### A-Rendering + A-Animation - -- [TextureAtlasBuilder now respects insertion order][11474] -- [normalize joint weights][10539] - -### A-ECS + A-Meta - -- [resolve all internal ambiguities][10411] - -### A-Rendering + A-UI - -- [Provide GlobalsUniform in UiMaterial shaders][10739] -- [Include UI node size in the vertex inputs for UiMaterial.][11722] -- [UI Texture 9 slice][11600] -- [Optional ImageScaleMode][11780] - -### A-Math - -- [Define a basic set of Primitives][10466] -- [Add and impl Primitives][10580] -- [Add winding order for `Triangle2d`][10620] -- [Use minor and major radii for `Torus` primitive shape][10643] -- [Remove `From` implementations from the direction types][10857] -- [Impl `TryFrom` vector for directions and add `InvalidDirectionError`][10884] -- [Add `Direction2d::from_xy` and `Direction3d::from_xyz`][10882] -- [Implement `Neg` for `Direction2d` and `Direction3d`][11179] -- [Add constants for `Direction2d` and `Direction3d`][11180] -- [Add `approx` feature to `bevy_math`][11176] -- [Add `libm` feature to `bevy_math`][11238] -- [Add `new_and_length` method to `Direction2d` and `Direction3d`][11172] -- [Update `glam`, `encase` and `hexasphere`][11082] -- [Implement bounding volume types][10946] -- [Remove `Default` impl for `CubicCurve`][11335] -- [Implement bounding volumes for primitive shapes][11336] -- [Improve `Rectangle` and `Cuboid` consistency][11434] -- [Change `Ellipse` representation and improve helpers][11435] -- [Add `Aabb2d::new` and `Aabb3d::new` constructors][11433] -- [Add geometric primitives to `bevy_math::prelude`][11432] -- [Direction: Rename `from_normalized` to `new_unchecked`][11425] -- [Implement bounding volume intersections][11439] -- [Add `new` constructors for `Circle` and `Sphere`][11526] -- [Derive PartialEq, Serialize, Deserialize and Reflect on primitives][11514] -- [Document RegularPolygon][11017] -- [Add RayTest2d and RayTest3d][11310] -- [Add more constructors and math helpers for primitive shapes][10632] -- [Add `Capsule2d` primitive][11585] -- [Add volume cast intersection tests][11586] -- [Add Clone to intersection test types][11640] -- [Implement `approx` traits for direction types][11650] -- [Support rotating `Direction3d` by `Quat`][11649] -- [Rename RayTest to RayCast][11635] -- [Add example for bounding volumes and intersection tests][11666] -- [Dedicated primitive example][11697] -- [Un-hardcode positions and colors in `2d_shapes` example][11867] - -### A-Build-System - -- [check for all-features with cargo-deny][10544] -- [Bump actions/github-script from 6 to 7][10653] -- [Add doc_markdown clippy linting config to cargo workspace][10640] -- [Enable `clippy::undocumented_unsafe_blocks` warning across the workspace][10646] -- [Remove trailing whitespace][10723] -- [Move remaining clippy lint definitions to Cargo.toml][10672] -- [Add `clippy::manual_let_else` at warn level to lints][10684] -- [Remove unused import][10963] -- [Rename functions and variables to follow code style][10961] -- [Remove unused variable][10966] -- [add libxkbcommon-x11-0 to the default linux dependencies][11060] -- [fix patches for example showcase after winit update][11058] -- [finish cleaning up dependency bans job][11059] -- [Bump actions/upload-artifact from 2 to 4][11014] -- [Publish dev-docs with Github Pages artifacts (2nd attempt)][10892] -- [example showcase patches: use default instead of game mode for desktop][11250] -- [Bump toml_edit in build-template-pages tool][11342] -- [Miri is failing on latest nightly: pin nightly to last known working version][11421] -- [Bump dev-docs pages actions][11418] -- [Unpin nightly for miri][11462] -- [documentation in CI: remove lock file][11507] -- [Bump actions/cache from 3 to 4][11469] -- [simplify animated_material example][11576] -- [example showcase: fix window resized patch][11596] -- [run examples on macOS to validate PRs][11630] -- [Inverse `missing_docs` logic][11676] -- [Bump peter-evans/create-pull-request from 5 to 6][11712] - -### A-Gizmos - -- [Fix float precision issue in the gizmo shader][10408] -- [Gizmo Arrows][10550] -- [Move Circle Gizmos to Their Own File][10631] -- [move gizmo arcs to their own file][10660] -- [Warn when bevy_sprite and bevy_pbr are not enabled with bevy_gizmos][11296] -- [Multiple Configurations for Gizmos][10342] -- [Fix gizmos app new panic][11420] -- [Use Direction3d for gizmos.circle normal][11422] -- [Implement Arc3D for Gizmos][11540] -- [Insert Gizmos config instead of Init][11580] -- [Drawing Primitives with Gizmos][11072] -- [fix(primitives): fix polygon gizmo rendering bug][11699] -- [Fix global wireframe behavior not being applied on new meshes][11792] -- [Overwrite gizmo group in `insert_gizmo_group`][11860] - -### A-Rendering + A-Math - -- [Split `Ray` into `Ray2d` and `Ray3d` and simplify plane construction][10856] -- [Introduce AspectRatio struct][10368] -- [Implement meshing for `Capsule2d`][11639] -- [Implement `Meshable` for some 3D primitives][11688] - -### A-Core - -- [Derive `Debug` for `Framecount`][11573] -- [Don't unconditionally enable bevy_render or bevy_assets if mutli-threaded feature is enabled][11726] - -### A-Windowing - -- [Some explanations for Window component][10714] -- [don't run update before window creation in winit][10741] -- [add new event `WindowOccluded` from winit][10735] -- [Add comment about scale factor in `WindowMode`][10872] -- [Refactor function `update_accessibility_nodes`][10911] -- [Change `Window` scale factor to f32 (adopted)][10897] -- [Reexport winit::platform::android::activity::* in bevy_winit][11011] -- [Update winit dependency to 0.29][10702] -- [Remove CanvasParentResizePlugin][11057] -- [Use `WindowBuilder::with_append()` to append canvas][11065] -- [Fix perf degradation on web builds][11227] -- [mobile and webgpu: trigger redraw request when needed and improve window creation][11245] -- [Remove unnecessary unsafe impls for WinitWindows on Wasm][11270] -- [Fix Reactive and ReactiveLowPower update modes][11325] -- [Change `WinitPlugin` defaults to limit game update rate when window is not visible (for real this time)][11305] -- [Cleanup bevy winit][11489] -- [Add `name` to `bevy::window::Window`][7650] -- [Avoid unwraps in winit fullscreen handling code][11735] - -### A-UI + A-Transform + A-Text - -- [UI text rotation and scaling fix][11326] - -### A-Animation - -- [Fix animations resetting after repeat count][10540] -- [Add Debug, PartialEq and Eq derives to bevy_animation.][10562] -- [support all types of animation interpolation from gltf][10755] -- [Clean up code to find the current keyframe][11306] -- [Skip alloc when updating animation path cache][11330] -- [Replace the `cubic_spline_interpolation` macro with a generic function][11605] -- [Animatable trait for interpolation and blending][4482] - -### A-ECS + A-Pointers - -- [Replace pointer castings (`as`) by their API equivalent][11818] - -### A-ECS + A-Utils - -- [Add helper macro's for logging only once][10808] -- [Move `EntityHash` related types into `bevy_ecs`][11498] - -### A-Reflection - -- [Fix issue with `Option` serialization][10705] -- [fix `insert_reflect` panic caused by `clone_value`][10627] -- [Remove pointless trait implementation exports in `bevy_reflect`][10771] -- [Fix nested generics in Reflect derive][10791] -- [Fix debug printing for dynamic types][10740] -- [reflect: maximally relax `TypePath` bounds][11037] -- [Use `static_assertions` to check for trait impls][11407] -- [Add `ReflectFromWorld` and replace the `FromWorld` requirement on `ReflectComponent` and `ReflectBundle` with `FromReflect`][9623] -- [Fix reflected serialization/deserialization on `Name` component][11447] -- [Add Reflection for Wrapping/Saturating types][11397] -- [Remove TypeUuid][11497] -- [Fix warnings in bevy_reflect][11556] -- [bevy_reflect: Type parameter bounds][9046] -- [bevy_reflect: Split `#[reflect(where)]`][11597] -- [reflection: replace `impl_reflect_struct` with `impl_reflect`][11437] -- [Add the ability to manually create ParsedPaths (+ cleanup)][11029] -- [bevy_reflect: Reflect `&'static str`][11686] -- [Improve DynamicStruct::insert][11068] -- [Missing registrations][11736] -- [Add `ReflectKind`][11664] -- [doc(bevy_reflect): add note about trait bounds on `impl_type_path`][11810] -- [bevy_reflect_derive: Clean up attribute logic][11777] - -### A-ECS + A-Tasks - -- [Async channel v2][10692] - -### A-Pointers - -- [Remove a ptr-to-int cast in `CommandQueue::apply`][10475] -- [Fix memory leak in dynamic ECS example][11461] -- [bevy_ptr: fix `unsafe_op_in_unsafe_fn` lint][11610] - -### A-ECS + A-Reflection - -- [Adding derive Reflect for tick structs][11641] - -### A-Reflection + A-Gizmos - -- [`#[derive(Reflect)]` on `GizmoConfig`][10483] -- [Register `WireframeColor`][10486] - -### No area label - -- [Fix intra-doc link warnings][10445] -- [Fix minor issues with custom_asset example][10337] -- [Prepend `root_path` to meta path in HttpWasmAssetReader][10527] -- [support required features in wasm examples showcase][10577] -- [examples showcase: use patches instead of sed for wasm hacks][10601] -- [Add [lints] table, fix adding `#![allow(clippy::type_complexity)]` everywhere][10011] -- [Bumps async crates requirements to latest major version][10370] -- [delete methods deprecated in 0.12][10693] -- [Ran `cargo fmt` on `benches` crate][10758] -- [Remove unnecessary path prefixes][10749] -- [Fix typos in safety comment][10827] -- [Substitute `get(0)` with `first()`][10847] -- [Remove identity `map` calls][10848] -- [Renamed Accessibility plugin to AccessKitPlugin in bevy_winit][10914] -- [Reorder impl to be the same as the trait][11076] -- [Replace deprecated elements][10999] -- [Remove unnecessary parentheses][10990] -- [Replace deprecated elements][10999] -- [Simplify equality assertions][10988] -- [Add Solus package requrements to linux_dependencies.md][10996] -- [Update base64 requirement from 0.13.0 to 0.21.5][10336] -- [Update sysinfo version to 0.30.0][11071] -- [Remove unnecessary parens][11075] -- [Reorder impl to be the same as the trait][11076] -- [Fix ci xvfb][11143] -- [Replace or document ignored doctests][11040] -- [Add static assertions to bevy_utils for compile-time checks][11182] -- [Fix missed explicit conversions in examples][11261] -- [Remove unused event-listener dependency][11269] -- [Fixed typo in generate_custom_mesh.rs example][11293] -- [Extract examples `CameraController` into a module][11338] -- [Use EntityHashMap whenever possible][11353] -- [Fix link to plugin guidelines][11379] -- [[doc] Fix typo and formatting in CONTRIBUTING.md][11381] -- [add a required feature for shader_material_glsl][11440] -- [Update ruzstd requirement from 0.4.0 to 0.5.0][11467] -- [Tweak gamepad viewer example style][11484] -- [Add `.toml` extension to `.cargo/config_fast_builds`][11506] -- [Add README to benches][11508] -- [Fix panic in examples using argh on the web][11513] -- [Fix cyclic dep][11523] -- [Enable the `unsafe_op_in_unsafe_fn` lint][11591] -- [Update erased-serde requirement from 0.3 to 0.4][11599] -- [Fix example send_and_receive_events][11615] -- [Update cursor.rs][11617] -- [Use the `Continuous` update mode in stress tests when unfocused][11652] -- [Don't auto insert on the extract schedule][11669] -- [Update tracing-tracy requirement from 0.10.4 to 0.11.0 and tracy-client requirement from 0.16.4 to 0.17.0][11678] -- [Use TypeIdMap whenever possible][11684] -- [Fix a few typos in error docs][11709] -- [bevy_render: use the non-send marker from bevy_core][11725] -- [Ignore screenshots generated by `screenshot` example][11797] -- [Docs reflect that `RemovalDetection` also yields despawned entities][11795] -- [bevy_dynamic_plugin: fix `unsafe_op_in_unsafe_fn` lint][11622] -- [Replace `crossbeam::scope` reference with `thread::scope` in docs][11832] -- [Use question mark operator when possible][11865] -- [Fix a few Clippy lints][11866] -- [WebGPU: fix web-sys version][11894] -- [Remove map_flatten from linting rules][11913] -- [Fix duplicate `encase_derive_impl` dependency][11915] - -### A-App - -- [add regression test for #10385/#10389][10609] -- [Fix typos plugin.rs][11193] -- [Expressively define plugins using functions][11080] -- [Mark `DynamicPluginLoadError` internal error types as source][11618] - -### A-Diagnostics - -- [Fix Line for tracy version][10663] -- [Some doc to bevy_diagnostic][11020] -- [Print to stderr from panic handler in LogPlugin][11170] -- [Add ability to panic to logs example][11171] -- [Make sure tracy deps conform to compatibility table][11331] -- [Describe purpose of bevy_diagnostic][11327] -- [Add support for updating the tracing subscriber in LogPlugin][10822] -- [Replace `DiagnosticId` by `DiagnosticPath`][9266] -- [fix link to tracy][11521] -- [Fix sysinfo CPU brand output][11850] - -### A-Rendering + A-ECS - -- [Explain where rendering is][11018] - -### A-Assets + A-Math - -- [Use glam for computing gLTF node transform][11361] - -[4482]: https://github.com/bevyengine/bevy/pull/4482 -[5103]: https://github.com/bevyengine/bevy/pull/5103 -[6773]: https://github.com/bevyengine/bevy/pull/6773 -[7650]: https://github.com/bevyengine/bevy/pull/7650 -[8112]: https://github.com/bevyengine/bevy/pull/8112 -[8453]: https://github.com/bevyengine/bevy/pull/8453 -[9046]: https://github.com/bevyengine/bevy/pull/9046 -[9172]: https://github.com/bevyengine/bevy/pull/9172 -[9266]: https://github.com/bevyengine/bevy/pull/9266 -[9623]: https://github.com/bevyengine/bevy/pull/9623 -[9774]: https://github.com/bevyengine/bevy/pull/9774 -[9797]: https://github.com/bevyengine/bevy/pull/9797 -[9822]: https://github.com/bevyengine/bevy/pull/9822 -[9907]: https://github.com/bevyengine/bevy/pull/9907 -[9918]: https://github.com/bevyengine/bevy/pull/9918 -[9920]: https://github.com/bevyengine/bevy/pull/9920 -[9943]: https://github.com/bevyengine/bevy/pull/9943 -[10011]: https://github.com/bevyengine/bevy/pull/10011 -[10073]: https://github.com/bevyengine/bevy/pull/10073 -[10077]: https://github.com/bevyengine/bevy/pull/10077 -[10153]: https://github.com/bevyengine/bevy/pull/10153 -[10224]: https://github.com/bevyengine/bevy/pull/10224 -[10231]: https://github.com/bevyengine/bevy/pull/10231 -[10266]: https://github.com/bevyengine/bevy/pull/10266 -[10268]: https://github.com/bevyengine/bevy/pull/10268 -[10321]: https://github.com/bevyengine/bevy/pull/10321 -[10325]: https://github.com/bevyengine/bevy/pull/10325 -[10336]: https://github.com/bevyengine/bevy/pull/10336 -[10337]: https://github.com/bevyengine/bevy/pull/10337 -[10342]: https://github.com/bevyengine/bevy/pull/10342 -[10368]: https://github.com/bevyengine/bevy/pull/10368 -[10369]: https://github.com/bevyengine/bevy/pull/10369 -[10370]: https://github.com/bevyengine/bevy/pull/10370 -[10376]: https://github.com/bevyengine/bevy/pull/10376 -[10378]: https://github.com/bevyengine/bevy/pull/10378 -[10380]: https://github.com/bevyengine/bevy/pull/10380 -[10383]: https://github.com/bevyengine/bevy/pull/10383 -[10389]: https://github.com/bevyengine/bevy/pull/10389 -[10394]: https://github.com/bevyengine/bevy/pull/10394 -[10400]: https://github.com/bevyengine/bevy/pull/10400 -[10408]: https://github.com/bevyengine/bevy/pull/10408 -[10410]: https://github.com/bevyengine/bevy/pull/10410 -[10411]: https://github.com/bevyengine/bevy/pull/10411 -[10415]: https://github.com/bevyengine/bevy/pull/10415 -[10419]: https://github.com/bevyengine/bevy/pull/10419 -[10421]: https://github.com/bevyengine/bevy/pull/10421 -[10422]: https://github.com/bevyengine/bevy/pull/10422 -[10423]: https://github.com/bevyengine/bevy/pull/10423 -[10424]: https://github.com/bevyengine/bevy/pull/10424 -[10434]: https://github.com/bevyengine/bevy/pull/10434 -[10441]: https://github.com/bevyengine/bevy/pull/10441 -[10442]: https://github.com/bevyengine/bevy/pull/10442 -[10445]: https://github.com/bevyengine/bevy/pull/10445 -[10446]: https://github.com/bevyengine/bevy/pull/10446 -[10448]: https://github.com/bevyengine/bevy/pull/10448 -[10450]: https://github.com/bevyengine/bevy/pull/10450 -[10452]: https://github.com/bevyengine/bevy/pull/10452 -[10453]: https://github.com/bevyengine/bevy/pull/10453 -[10454]: https://github.com/bevyengine/bevy/pull/10454 -[10461]: https://github.com/bevyengine/bevy/pull/10461 -[10466]: https://github.com/bevyengine/bevy/pull/10466 -[10474]: https://github.com/bevyengine/bevy/pull/10474 -[10475]: https://github.com/bevyengine/bevy/pull/10475 -[10483]: https://github.com/bevyengine/bevy/pull/10483 -[10484]: https://github.com/bevyengine/bevy/pull/10484 -[10485]: https://github.com/bevyengine/bevy/pull/10485 -[10486]: https://github.com/bevyengine/bevy/pull/10486 -[10493]: https://github.com/bevyengine/bevy/pull/10493 -[10494]: https://github.com/bevyengine/bevy/pull/10494 -[10496]: https://github.com/bevyengine/bevy/pull/10496 -[10502]: https://github.com/bevyengine/bevy/pull/10502 -[10514]: https://github.com/bevyengine/bevy/pull/10514 -[10516]: https://github.com/bevyengine/bevy/pull/10516 -[10517]: https://github.com/bevyengine/bevy/pull/10517 -[10519]: https://github.com/bevyengine/bevy/pull/10519 -[10520]: https://github.com/bevyengine/bevy/pull/10520 -[10523]: https://github.com/bevyengine/bevy/pull/10523 -[10526]: https://github.com/bevyengine/bevy/pull/10526 -[10527]: https://github.com/bevyengine/bevy/pull/10527 -[10529]: https://github.com/bevyengine/bevy/pull/10529 -[10534]: https://github.com/bevyengine/bevy/pull/10534 -[10537]: https://github.com/bevyengine/bevy/pull/10537 -[10539]: https://github.com/bevyengine/bevy/pull/10539 -[10540]: https://github.com/bevyengine/bevy/pull/10540 -[10542]: https://github.com/bevyengine/bevy/pull/10542 -[10543]: https://github.com/bevyengine/bevy/pull/10543 -[10544]: https://github.com/bevyengine/bevy/pull/10544 -[10547]: https://github.com/bevyengine/bevy/pull/10547 -[10548]: https://github.com/bevyengine/bevy/pull/10548 -[10550]: https://github.com/bevyengine/bevy/pull/10550 -[10551]: https://github.com/bevyengine/bevy/pull/10551 -[10555]: https://github.com/bevyengine/bevy/pull/10555 -[10556]: https://github.com/bevyengine/bevy/pull/10556 -[10558]: https://github.com/bevyengine/bevy/pull/10558 -[10559]: https://github.com/bevyengine/bevy/pull/10559 -[10560]: https://github.com/bevyengine/bevy/pull/10560 -[10561]: https://github.com/bevyengine/bevy/pull/10561 -[10562]: https://github.com/bevyengine/bevy/pull/10562 -[10563]: https://github.com/bevyengine/bevy/pull/10563 -[10564]: https://github.com/bevyengine/bevy/pull/10564 -[10565]: https://github.com/bevyengine/bevy/pull/10565 -[10574]: https://github.com/bevyengine/bevy/pull/10574 -[10577]: https://github.com/bevyengine/bevy/pull/10577 -[10578]: https://github.com/bevyengine/bevy/pull/10578 -[10580]: https://github.com/bevyengine/bevy/pull/10580 -[10585]: https://github.com/bevyengine/bevy/pull/10585 -[10588]: https://github.com/bevyengine/bevy/pull/10588 -[10589]: https://github.com/bevyengine/bevy/pull/10589 -[10591]: https://github.com/bevyengine/bevy/pull/10591 -[10592]: https://github.com/bevyengine/bevy/pull/10592 -[10594]: https://github.com/bevyengine/bevy/pull/10594 -[10596]: https://github.com/bevyengine/bevy/pull/10596 -[10598]: https://github.com/bevyengine/bevy/pull/10598 -[10601]: https://github.com/bevyengine/bevy/pull/10601 -[10608]: https://github.com/bevyengine/bevy/pull/10608 -[10609]: https://github.com/bevyengine/bevy/pull/10609 -[10610]: https://github.com/bevyengine/bevy/pull/10610 -[10611]: https://github.com/bevyengine/bevy/pull/10611 -[10614]: https://github.com/bevyengine/bevy/pull/10614 -[10619]: https://github.com/bevyengine/bevy/pull/10619 -[10620]: https://github.com/bevyengine/bevy/pull/10620 -[10623]: https://github.com/bevyengine/bevy/pull/10623 -[10624]: https://github.com/bevyengine/bevy/pull/10624 -[10627]: https://github.com/bevyengine/bevy/pull/10627 -[10628]: https://github.com/bevyengine/bevy/pull/10628 -[10631]: https://github.com/bevyengine/bevy/pull/10631 -[10632]: https://github.com/bevyengine/bevy/pull/10632 -[10633]: https://github.com/bevyengine/bevy/pull/10633 -[10638]: https://github.com/bevyengine/bevy/pull/10638 -[10640]: https://github.com/bevyengine/bevy/pull/10640 -[10643]: https://github.com/bevyengine/bevy/pull/10643 -[10644]: https://github.com/bevyengine/bevy/pull/10644 -[10646]: https://github.com/bevyengine/bevy/pull/10646 -[10648]: https://github.com/bevyengine/bevy/pull/10648 -[10650]: https://github.com/bevyengine/bevy/pull/10650 -[10651]: https://github.com/bevyengine/bevy/pull/10651 -[10653]: https://github.com/bevyengine/bevy/pull/10653 -[10657]: https://github.com/bevyengine/bevy/pull/10657 -[10660]: https://github.com/bevyengine/bevy/pull/10660 -[10663]: https://github.com/bevyengine/bevy/pull/10663 -[10670]: https://github.com/bevyengine/bevy/pull/10670 -[10672]: https://github.com/bevyengine/bevy/pull/10672 -[10674]: https://github.com/bevyengine/bevy/pull/10674 -[10675]: https://github.com/bevyengine/bevy/pull/10675 -[10681]: https://github.com/bevyengine/bevy/pull/10681 -[10682]: https://github.com/bevyengine/bevy/pull/10682 -[10683]: https://github.com/bevyengine/bevy/pull/10683 -[10684]: https://github.com/bevyengine/bevy/pull/10684 -[10691]: https://github.com/bevyengine/bevy/pull/10691 -[10692]: https://github.com/bevyengine/bevy/pull/10692 -[10693]: https://github.com/bevyengine/bevy/pull/10693 -[10698]: https://github.com/bevyengine/bevy/pull/10698 -[10699]: https://github.com/bevyengine/bevy/pull/10699 -[10702]: https://github.com/bevyengine/bevy/pull/10702 -[10705]: https://github.com/bevyengine/bevy/pull/10705 -[10706]: https://github.com/bevyengine/bevy/pull/10706 -[10712]: https://github.com/bevyengine/bevy/pull/10712 -[10714]: https://github.com/bevyengine/bevy/pull/10714 -[10719]: https://github.com/bevyengine/bevy/pull/10719 -[10721]: https://github.com/bevyengine/bevy/pull/10721 -[10722]: https://github.com/bevyengine/bevy/pull/10722 -[10723]: https://github.com/bevyengine/bevy/pull/10723 -[10730]: https://github.com/bevyengine/bevy/pull/10730 -[10733]: https://github.com/bevyengine/bevy/pull/10733 -[10734]: https://github.com/bevyengine/bevy/pull/10734 -[10735]: https://github.com/bevyengine/bevy/pull/10735 -[10736]: https://github.com/bevyengine/bevy/pull/10736 -[10739]: https://github.com/bevyengine/bevy/pull/10739 -[10740]: https://github.com/bevyengine/bevy/pull/10740 -[10741]: https://github.com/bevyengine/bevy/pull/10741 -[10742]: https://github.com/bevyengine/bevy/pull/10742 -[10745]: https://github.com/bevyengine/bevy/pull/10745 -[10746]: https://github.com/bevyengine/bevy/pull/10746 -[10748]: https://github.com/bevyengine/bevy/pull/10748 -[10749]: https://github.com/bevyengine/bevy/pull/10749 -[10755]: https://github.com/bevyengine/bevy/pull/10755 -[10758]: https://github.com/bevyengine/bevy/pull/10758 -[10764]: https://github.com/bevyengine/bevy/pull/10764 -[10770]: https://github.com/bevyengine/bevy/pull/10770 -[10771]: https://github.com/bevyengine/bevy/pull/10771 -[10774]: https://github.com/bevyengine/bevy/pull/10774 -[10779]: https://github.com/bevyengine/bevy/pull/10779 -[10780]: https://github.com/bevyengine/bevy/pull/10780 -[10782]: https://github.com/bevyengine/bevy/pull/10782 -[10785]: https://github.com/bevyengine/bevy/pull/10785 -[10787]: https://github.com/bevyengine/bevy/pull/10787 -[10788]: https://github.com/bevyengine/bevy/pull/10788 -[10791]: https://github.com/bevyengine/bevy/pull/10791 -[10799]: https://github.com/bevyengine/bevy/pull/10799 -[10801]: https://github.com/bevyengine/bevy/pull/10801 -[10804]: https://github.com/bevyengine/bevy/pull/10804 -[10808]: https://github.com/bevyengine/bevy/pull/10808 -[10811]: https://github.com/bevyengine/bevy/pull/10811 -[10812]: https://github.com/bevyengine/bevy/pull/10812 -[10815]: https://github.com/bevyengine/bevy/pull/10815 -[10818]: https://github.com/bevyengine/bevy/pull/10818 -[10822]: https://github.com/bevyengine/bevy/pull/10822 -[10827]: https://github.com/bevyengine/bevy/pull/10827 -[10836]: https://github.com/bevyengine/bevy/pull/10836 -[10837]: https://github.com/bevyengine/bevy/pull/10837 -[10841]: https://github.com/bevyengine/bevy/pull/10841 -[10847]: https://github.com/bevyengine/bevy/pull/10847 -[10848]: https://github.com/bevyengine/bevy/pull/10848 -[10854]: https://github.com/bevyengine/bevy/pull/10854 -[10856]: https://github.com/bevyengine/bevy/pull/10856 -[10857]: https://github.com/bevyengine/bevy/pull/10857 -[10859]: https://github.com/bevyengine/bevy/pull/10859 -[10872]: https://github.com/bevyengine/bevy/pull/10872 -[10873]: https://github.com/bevyengine/bevy/pull/10873 -[10878]: https://github.com/bevyengine/bevy/pull/10878 -[10879]: https://github.com/bevyengine/bevy/pull/10879 -[10882]: https://github.com/bevyengine/bevy/pull/10882 -[10884]: https://github.com/bevyengine/bevy/pull/10884 -[10885]: https://github.com/bevyengine/bevy/pull/10885 -[10889]: https://github.com/bevyengine/bevy/pull/10889 -[10892]: https://github.com/bevyengine/bevy/pull/10892 -[10893]: https://github.com/bevyengine/bevy/pull/10893 -[10897]: https://github.com/bevyengine/bevy/pull/10897 -[10905]: https://github.com/bevyengine/bevy/pull/10905 -[10906]: https://github.com/bevyengine/bevy/pull/10906 -[10910]: https://github.com/bevyengine/bevy/pull/10910 -[10911]: https://github.com/bevyengine/bevy/pull/10911 -[10914]: https://github.com/bevyengine/bevy/pull/10914 -[10918]: https://github.com/bevyengine/bevy/pull/10918 -[10920]: https://github.com/bevyengine/bevy/pull/10920 -[10922]: https://github.com/bevyengine/bevy/pull/10922 -[10927]: https://github.com/bevyengine/bevy/pull/10927 -[10930]: https://github.com/bevyengine/bevy/pull/10930 -[10937]: https://github.com/bevyengine/bevy/pull/10937 -[10939]: https://github.com/bevyengine/bevy/pull/10939 -[10941]: https://github.com/bevyengine/bevy/pull/10941 -[10942]: https://github.com/bevyengine/bevy/pull/10942 -[10943]: https://github.com/bevyengine/bevy/pull/10943 -[10946]: https://github.com/bevyengine/bevy/pull/10946 -[10947]: https://github.com/bevyengine/bevy/pull/10947 -[10951]: https://github.com/bevyengine/bevy/pull/10951 -[10961]: https://github.com/bevyengine/bevy/pull/10961 -[10962]: https://github.com/bevyengine/bevy/pull/10962 -[10963]: https://github.com/bevyengine/bevy/pull/10963 -[10965]: https://github.com/bevyengine/bevy/pull/10965 -[10966]: https://github.com/bevyengine/bevy/pull/10966 -[10967]: https://github.com/bevyengine/bevy/pull/10967 -[10969]: https://github.com/bevyengine/bevy/pull/10969 -[10970]: https://github.com/bevyengine/bevy/pull/10970 -[10971]: https://github.com/bevyengine/bevy/pull/10971 -[10977]: https://github.com/bevyengine/bevy/pull/10977 -[10979]: https://github.com/bevyengine/bevy/pull/10979 -[10980]: https://github.com/bevyengine/bevy/pull/10980 -[10988]: https://github.com/bevyengine/bevy/pull/10988 -[10989]: https://github.com/bevyengine/bevy/pull/10989 -[10990]: https://github.com/bevyengine/bevy/pull/10990 -[10996]: https://github.com/bevyengine/bevy/pull/10996 -[10998]: https://github.com/bevyengine/bevy/pull/10998 -[10999]: https://github.com/bevyengine/bevy/pull/10999 -[11002]: https://github.com/bevyengine/bevy/pull/11002 -[11003]: https://github.com/bevyengine/bevy/pull/11003 -[11006]: https://github.com/bevyengine/bevy/pull/11006 -[11010]: https://github.com/bevyengine/bevy/pull/11010 -[11011]: https://github.com/bevyengine/bevy/pull/11011 -[11012]: https://github.com/bevyengine/bevy/pull/11012 -[11014]: https://github.com/bevyengine/bevy/pull/11014 -[11016]: https://github.com/bevyengine/bevy/pull/11016 -[11017]: https://github.com/bevyengine/bevy/pull/11017 -[11018]: https://github.com/bevyengine/bevy/pull/11018 -[11020]: https://github.com/bevyengine/bevy/pull/11020 -[11021]: https://github.com/bevyengine/bevy/pull/11021 -[11023]: https://github.com/bevyengine/bevy/pull/11023 -[11024]: https://github.com/bevyengine/bevy/pull/11024 -[11025]: https://github.com/bevyengine/bevy/pull/11025 -[11027]: https://github.com/bevyengine/bevy/pull/11027 -[11028]: https://github.com/bevyengine/bevy/pull/11028 -[11029]: https://github.com/bevyengine/bevy/pull/11029 -[11030]: https://github.com/bevyengine/bevy/pull/11030 -[11031]: https://github.com/bevyengine/bevy/pull/11031 -[11037]: https://github.com/bevyengine/bevy/pull/11037 -[11040]: https://github.com/bevyengine/bevy/pull/11040 -[11041]: https://github.com/bevyengine/bevy/pull/11041 -[11043]: https://github.com/bevyengine/bevy/pull/11043 -[11045]: https://github.com/bevyengine/bevy/pull/11045 -[11049]: https://github.com/bevyengine/bevy/pull/11049 -[11051]: https://github.com/bevyengine/bevy/pull/11051 -[11053]: https://github.com/bevyengine/bevy/pull/11053 -[11054]: https://github.com/bevyengine/bevy/pull/11054 -[11057]: https://github.com/bevyengine/bevy/pull/11057 -[11058]: https://github.com/bevyengine/bevy/pull/11058 -[11059]: https://github.com/bevyengine/bevy/pull/11059 -[11060]: https://github.com/bevyengine/bevy/pull/11060 -[11063]: https://github.com/bevyengine/bevy/pull/11063 -[11064]: https://github.com/bevyengine/bevy/pull/11064 -[11065]: https://github.com/bevyengine/bevy/pull/11065 -[11068]: https://github.com/bevyengine/bevy/pull/11068 -[11069]: https://github.com/bevyengine/bevy/pull/11069 -[11071]: https://github.com/bevyengine/bevy/pull/11071 -[11072]: https://github.com/bevyengine/bevy/pull/11072 -[11075]: https://github.com/bevyengine/bevy/pull/11075 -[11076]: https://github.com/bevyengine/bevy/pull/11076 -[11077]: https://github.com/bevyengine/bevy/pull/11077 -[11080]: https://github.com/bevyengine/bevy/pull/11080 -[11082]: https://github.com/bevyengine/bevy/pull/11082 -[11084]: https://github.com/bevyengine/bevy/pull/11084 -[11089]: https://github.com/bevyengine/bevy/pull/11089 -[11092]: https://github.com/bevyengine/bevy/pull/11092 -[11095]: https://github.com/bevyengine/bevy/pull/11095 -[11103]: https://github.com/bevyengine/bevy/pull/11103 -[11104]: https://github.com/bevyengine/bevy/pull/11104 -[11106]: https://github.com/bevyengine/bevy/pull/11106 -[11107]: https://github.com/bevyengine/bevy/pull/11107 -[11110]: https://github.com/bevyengine/bevy/pull/11110 -[11118]: https://github.com/bevyengine/bevy/pull/11118 -[11128]: https://github.com/bevyengine/bevy/pull/11128 -[11129]: https://github.com/bevyengine/bevy/pull/11129 -[11134]: https://github.com/bevyengine/bevy/pull/11134 -[11138]: https://github.com/bevyengine/bevy/pull/11138 -[11139]: https://github.com/bevyengine/bevy/pull/11139 -[11140]: https://github.com/bevyengine/bevy/pull/11140 -[11142]: https://github.com/bevyengine/bevy/pull/11142 -[11143]: https://github.com/bevyengine/bevy/pull/11143 -[11146]: https://github.com/bevyengine/bevy/pull/11146 -[11149]: https://github.com/bevyengine/bevy/pull/11149 -[11152]: https://github.com/bevyengine/bevy/pull/11152 -[11153]: https://github.com/bevyengine/bevy/pull/11153 -[11162]: https://github.com/bevyengine/bevy/pull/11162 -[11163]: https://github.com/bevyengine/bevy/pull/11163 -[11164]: https://github.com/bevyengine/bevy/pull/11164 -[11166]: https://github.com/bevyengine/bevy/pull/11166 -[11167]: https://github.com/bevyengine/bevy/pull/11167 -[11170]: https://github.com/bevyengine/bevy/pull/11170 -[11171]: https://github.com/bevyengine/bevy/pull/11171 -[11172]: https://github.com/bevyengine/bevy/pull/11172 -[11176]: https://github.com/bevyengine/bevy/pull/11176 -[11178]: https://github.com/bevyengine/bevy/pull/11178 -[11179]: https://github.com/bevyengine/bevy/pull/11179 -[11180]: https://github.com/bevyengine/bevy/pull/11180 -[11182]: https://github.com/bevyengine/bevy/pull/11182 -[11188]: https://github.com/bevyengine/bevy/pull/11188 -[11189]: https://github.com/bevyengine/bevy/pull/11189 -[11191]: https://github.com/bevyengine/bevy/pull/11191 -[11192]: https://github.com/bevyengine/bevy/pull/11192 -[11193]: https://github.com/bevyengine/bevy/pull/11193 -[11194]: https://github.com/bevyengine/bevy/pull/11194 -[11195]: https://github.com/bevyengine/bevy/pull/11195 -[11199]: https://github.com/bevyengine/bevy/pull/11199 -[11205]: https://github.com/bevyengine/bevy/pull/11205 -[11206]: https://github.com/bevyengine/bevy/pull/11206 -[11212]: https://github.com/bevyengine/bevy/pull/11212 -[11218]: https://github.com/bevyengine/bevy/pull/11218 -[11226]: https://github.com/bevyengine/bevy/pull/11226 -[11227]: https://github.com/bevyengine/bevy/pull/11227 -[11234]: https://github.com/bevyengine/bevy/pull/11234 -[11238]: https://github.com/bevyengine/bevy/pull/11238 -[11239]: https://github.com/bevyengine/bevy/pull/11239 -[11242]: https://github.com/bevyengine/bevy/pull/11242 -[11243]: https://github.com/bevyengine/bevy/pull/11243 -[11245]: https://github.com/bevyengine/bevy/pull/11245 -[11248]: https://github.com/bevyengine/bevy/pull/11248 -[11250]: https://github.com/bevyengine/bevy/pull/11250 -[11254]: https://github.com/bevyengine/bevy/pull/11254 -[11260]: https://github.com/bevyengine/bevy/pull/11260 -[11261]: https://github.com/bevyengine/bevy/pull/11261 -[11268]: https://github.com/bevyengine/bevy/pull/11268 -[11269]: https://github.com/bevyengine/bevy/pull/11269 -[11270]: https://github.com/bevyengine/bevy/pull/11270 -[11280]: https://github.com/bevyengine/bevy/pull/11280 -[11284]: https://github.com/bevyengine/bevy/pull/11284 -[11289]: https://github.com/bevyengine/bevy/pull/11289 -[11292]: https://github.com/bevyengine/bevy/pull/11292 -[11293]: https://github.com/bevyengine/bevy/pull/11293 -[11296]: https://github.com/bevyengine/bevy/pull/11296 -[11305]: https://github.com/bevyengine/bevy/pull/11305 -[11306]: https://github.com/bevyengine/bevy/pull/11306 -[11307]: https://github.com/bevyengine/bevy/pull/11307 -[11310]: https://github.com/bevyengine/bevy/pull/11310 -[11313]: https://github.com/bevyengine/bevy/pull/11313 -[11316]: https://github.com/bevyengine/bevy/pull/11316 -[11319]: https://github.com/bevyengine/bevy/pull/11319 -[11321]: https://github.com/bevyengine/bevy/pull/11321 -[11323]: https://github.com/bevyengine/bevy/pull/11323 -[11325]: https://github.com/bevyengine/bevy/pull/11325 -[11326]: https://github.com/bevyengine/bevy/pull/11326 -[11327]: https://github.com/bevyengine/bevy/pull/11327 -[11330]: https://github.com/bevyengine/bevy/pull/11330 -[11331]: https://github.com/bevyengine/bevy/pull/11331 -[11332]: https://github.com/bevyengine/bevy/pull/11332 -[11334]: https://github.com/bevyengine/bevy/pull/11334 -[11335]: https://github.com/bevyengine/bevy/pull/11335 -[11336]: https://github.com/bevyengine/bevy/pull/11336 -[11338]: https://github.com/bevyengine/bevy/pull/11338 -[11342]: https://github.com/bevyengine/bevy/pull/11342 -[11347]: https://github.com/bevyengine/bevy/pull/11347 -[11353]: https://github.com/bevyengine/bevy/pull/11353 -[11360]: https://github.com/bevyengine/bevy/pull/11360 -[11361]: https://github.com/bevyengine/bevy/pull/11361 -[11366]: https://github.com/bevyengine/bevy/pull/11366 -[11368]: https://github.com/bevyengine/bevy/pull/11368 -[11369]: https://github.com/bevyengine/bevy/pull/11369 -[11370]: https://github.com/bevyengine/bevy/pull/11370 -[11371]: https://github.com/bevyengine/bevy/pull/11371 -[11373]: https://github.com/bevyengine/bevy/pull/11373 -[11379]: https://github.com/bevyengine/bevy/pull/11379 -[11381]: https://github.com/bevyengine/bevy/pull/11381 -[11383]: https://github.com/bevyengine/bevy/pull/11383 -[11386]: https://github.com/bevyengine/bevy/pull/11386 -[11388]: https://github.com/bevyengine/bevy/pull/11388 -[11389]: https://github.com/bevyengine/bevy/pull/11389 -[11391]: https://github.com/bevyengine/bevy/pull/11391 -[11397]: https://github.com/bevyengine/bevy/pull/11397 -[11399]: https://github.com/bevyengine/bevy/pull/11399 -[11400]: https://github.com/bevyengine/bevy/pull/11400 -[11403]: https://github.com/bevyengine/bevy/pull/11403 -[11404]: https://github.com/bevyengine/bevy/pull/11404 -[11405]: https://github.com/bevyengine/bevy/pull/11405 -[11407]: https://github.com/bevyengine/bevy/pull/11407 -[11412]: https://github.com/bevyengine/bevy/pull/11412 -[11416]: https://github.com/bevyengine/bevy/pull/11416 -[11417]: https://github.com/bevyengine/bevy/pull/11417 -[11418]: https://github.com/bevyengine/bevy/pull/11418 -[11419]: https://github.com/bevyengine/bevy/pull/11419 -[11420]: https://github.com/bevyengine/bevy/pull/11420 -[11421]: https://github.com/bevyengine/bevy/pull/11421 -[11422]: https://github.com/bevyengine/bevy/pull/11422 -[11425]: https://github.com/bevyengine/bevy/pull/11425 -[11428]: https://github.com/bevyengine/bevy/pull/11428 -[11431]: https://github.com/bevyengine/bevy/pull/11431 -[11432]: https://github.com/bevyengine/bevy/pull/11432 -[11433]: https://github.com/bevyengine/bevy/pull/11433 -[11434]: https://github.com/bevyengine/bevy/pull/11434 -[11435]: https://github.com/bevyengine/bevy/pull/11435 -[11436]: https://github.com/bevyengine/bevy/pull/11436 -[11437]: https://github.com/bevyengine/bevy/pull/11437 -[11439]: https://github.com/bevyengine/bevy/pull/11439 -[11440]: https://github.com/bevyengine/bevy/pull/11440 -[11442]: https://github.com/bevyengine/bevy/pull/11442 -[11444]: https://github.com/bevyengine/bevy/pull/11444 -[11445]: https://github.com/bevyengine/bevy/pull/11445 -[11447]: https://github.com/bevyengine/bevy/pull/11447 -[11454]: https://github.com/bevyengine/bevy/pull/11454 -[11455]: https://github.com/bevyengine/bevy/pull/11455 -[11456]: https://github.com/bevyengine/bevy/pull/11456 -[11461]: https://github.com/bevyengine/bevy/pull/11461 -[11462]: https://github.com/bevyengine/bevy/pull/11462 -[11467]: https://github.com/bevyengine/bevy/pull/11467 -[11469]: https://github.com/bevyengine/bevy/pull/11469 -[11474]: https://github.com/bevyengine/bevy/pull/11474 -[11480]: https://github.com/bevyengine/bevy/pull/11480 -[11483]: https://github.com/bevyengine/bevy/pull/11483 -[11484]: https://github.com/bevyengine/bevy/pull/11484 -[11486]: https://github.com/bevyengine/bevy/pull/11486 -[11487]: https://github.com/bevyengine/bevy/pull/11487 -[11489]: https://github.com/bevyengine/bevy/pull/11489 -[11491]: https://github.com/bevyengine/bevy/pull/11491 -[11497]: https://github.com/bevyengine/bevy/pull/11497 -[11498]: https://github.com/bevyengine/bevy/pull/11498 -[11499]: https://github.com/bevyengine/bevy/pull/11499 -[11500]: https://github.com/bevyengine/bevy/pull/11500 -[11504]: https://github.com/bevyengine/bevy/pull/11504 -[11506]: https://github.com/bevyengine/bevy/pull/11506 -[11507]: https://github.com/bevyengine/bevy/pull/11507 -[11508]: https://github.com/bevyengine/bevy/pull/11508 -[11512]: https://github.com/bevyengine/bevy/pull/11512 -[11513]: https://github.com/bevyengine/bevy/pull/11513 -[11514]: https://github.com/bevyengine/bevy/pull/11514 -[11519]: https://github.com/bevyengine/bevy/pull/11519 -[11521]: https://github.com/bevyengine/bevy/pull/11521 -[11523]: https://github.com/bevyengine/bevy/pull/11523 -[11524]: https://github.com/bevyengine/bevy/pull/11524 -[11526]: https://github.com/bevyengine/bevy/pull/11526 -[11527]: https://github.com/bevyengine/bevy/pull/11527 -[11528]: https://github.com/bevyengine/bevy/pull/11528 -[11529]: https://github.com/bevyengine/bevy/pull/11529 -[11531]: https://github.com/bevyengine/bevy/pull/11531 -[11534]: https://github.com/bevyengine/bevy/pull/11534 -[11538]: https://github.com/bevyengine/bevy/pull/11538 -[11540]: https://github.com/bevyengine/bevy/pull/11540 -[11541]: https://github.com/bevyengine/bevy/pull/11541 -[11543]: https://github.com/bevyengine/bevy/pull/11543 -[11548]: https://github.com/bevyengine/bevy/pull/11548 -[11551]: https://github.com/bevyengine/bevy/pull/11551 -[11555]: https://github.com/bevyengine/bevy/pull/11555 -[11556]: https://github.com/bevyengine/bevy/pull/11556 -[11560]: https://github.com/bevyengine/bevy/pull/11560 -[11561]: https://github.com/bevyengine/bevy/pull/11561 -[11573]: https://github.com/bevyengine/bevy/pull/11573 -[11574]: https://github.com/bevyengine/bevy/pull/11574 -[11575]: https://github.com/bevyengine/bevy/pull/11575 -[11576]: https://github.com/bevyengine/bevy/pull/11576 -[11578]: https://github.com/bevyengine/bevy/pull/11578 -[11580]: https://github.com/bevyengine/bevy/pull/11580 -[11581]: https://github.com/bevyengine/bevy/pull/11581 -[11583]: https://github.com/bevyengine/bevy/pull/11583 -[11585]: https://github.com/bevyengine/bevy/pull/11585 -[11586]: https://github.com/bevyengine/bevy/pull/11586 -[11591]: https://github.com/bevyengine/bevy/pull/11591 -[11596]: https://github.com/bevyengine/bevy/pull/11596 -[11597]: https://github.com/bevyengine/bevy/pull/11597 -[11599]: https://github.com/bevyengine/bevy/pull/11599 -[11600]: https://github.com/bevyengine/bevy/pull/11600 -[11604]: https://github.com/bevyengine/bevy/pull/11604 -[11605]: https://github.com/bevyengine/bevy/pull/11605 -[11610]: https://github.com/bevyengine/bevy/pull/11610 -[11611]: https://github.com/bevyengine/bevy/pull/11611 -[11612]: https://github.com/bevyengine/bevy/pull/11612 -[11615]: https://github.com/bevyengine/bevy/pull/11615 -[11616]: https://github.com/bevyengine/bevy/pull/11616 -[11617]: https://github.com/bevyengine/bevy/pull/11617 -[11618]: https://github.com/bevyengine/bevy/pull/11618 -[11622]: https://github.com/bevyengine/bevy/pull/11622 -[11626]: https://github.com/bevyengine/bevy/pull/11626 -[11627]: https://github.com/bevyengine/bevy/pull/11627 -[11630]: https://github.com/bevyengine/bevy/pull/11630 -[11635]: https://github.com/bevyengine/bevy/pull/11635 -[11639]: https://github.com/bevyengine/bevy/pull/11639 -[11640]: https://github.com/bevyengine/bevy/pull/11640 -[11641]: https://github.com/bevyengine/bevy/pull/11641 -[11644]: https://github.com/bevyengine/bevy/pull/11644 -[11645]: https://github.com/bevyengine/bevy/pull/11645 -[11649]: https://github.com/bevyengine/bevy/pull/11649 -[11650]: https://github.com/bevyengine/bevy/pull/11650 -[11652]: https://github.com/bevyengine/bevy/pull/11652 -[11660]: https://github.com/bevyengine/bevy/pull/11660 -[11662]: https://github.com/bevyengine/bevy/pull/11662 -[11664]: https://github.com/bevyengine/bevy/pull/11664 -[11666]: https://github.com/bevyengine/bevy/pull/11666 -[11669]: https://github.com/bevyengine/bevy/pull/11669 -[11671]: https://github.com/bevyengine/bevy/pull/11671 -[11672]: https://github.com/bevyengine/bevy/pull/11672 -[11675]: https://github.com/bevyengine/bevy/pull/11675 -[11676]: https://github.com/bevyengine/bevy/pull/11676 -[11678]: https://github.com/bevyengine/bevy/pull/11678 -[11684]: https://github.com/bevyengine/bevy/pull/11684 -[11686]: https://github.com/bevyengine/bevy/pull/11686 -[11687]: https://github.com/bevyengine/bevy/pull/11687 -[11688]: https://github.com/bevyengine/bevy/pull/11688 -[11690]: https://github.com/bevyengine/bevy/pull/11690 -[11693]: https://github.com/bevyengine/bevy/pull/11693 -[11697]: https://github.com/bevyengine/bevy/pull/11697 -[11699]: https://github.com/bevyengine/bevy/pull/11699 -[11700]: https://github.com/bevyengine/bevy/pull/11700 -[11703]: https://github.com/bevyengine/bevy/pull/11703 -[11705]: https://github.com/bevyengine/bevy/pull/11705 -[11709]: https://github.com/bevyengine/bevy/pull/11709 -[11710]: https://github.com/bevyengine/bevy/pull/11710 -[11712]: https://github.com/bevyengine/bevy/pull/11712 -[11720]: https://github.com/bevyengine/bevy/pull/11720 -[11721]: https://github.com/bevyengine/bevy/pull/11721 -[11722]: https://github.com/bevyengine/bevy/pull/11722 -[11725]: https://github.com/bevyengine/bevy/pull/11725 -[11726]: https://github.com/bevyengine/bevy/pull/11726 -[11728]: https://github.com/bevyengine/bevy/pull/11728 -[11733]: https://github.com/bevyengine/bevy/pull/11733 -[11735]: https://github.com/bevyengine/bevy/pull/11735 -[11736]: https://github.com/bevyengine/bevy/pull/11736 -[11737]: https://github.com/bevyengine/bevy/pull/11737 -[11745]: https://github.com/bevyengine/bevy/pull/11745 -[11747]: https://github.com/bevyengine/bevy/pull/11747 -[11751]: https://github.com/bevyengine/bevy/pull/11751 -[11758]: https://github.com/bevyengine/bevy/pull/11758 -[11764]: https://github.com/bevyengine/bevy/pull/11764 -[11767]: https://github.com/bevyengine/bevy/pull/11767 -[11769]: https://github.com/bevyengine/bevy/pull/11769 -[11773]: https://github.com/bevyengine/bevy/pull/11773 -[11777]: https://github.com/bevyengine/bevy/pull/11777 -[11780]: https://github.com/bevyengine/bevy/pull/11780 -[11781]: https://github.com/bevyengine/bevy/pull/11781 -[11783]: https://github.com/bevyengine/bevy/pull/11783 -[11785]: https://github.com/bevyengine/bevy/pull/11785 -[11791]: https://github.com/bevyengine/bevy/pull/11791 -[11792]: https://github.com/bevyengine/bevy/pull/11792 -[11795]: https://github.com/bevyengine/bevy/pull/11795 -[11797]: https://github.com/bevyengine/bevy/pull/11797 -[11798]: https://github.com/bevyengine/bevy/pull/11798 -[11800]: https://github.com/bevyengine/bevy/pull/11800 -[11801]: https://github.com/bevyengine/bevy/pull/11801 -[11803]: https://github.com/bevyengine/bevy/pull/11803 -[11805]: https://github.com/bevyengine/bevy/pull/11805 -[11810]: https://github.com/bevyengine/bevy/pull/11810 -[11818]: https://github.com/bevyengine/bevy/pull/11818 -[11822]: https://github.com/bevyengine/bevy/pull/11822 -[11832]: https://github.com/bevyengine/bevy/pull/11832 -[11838]: https://github.com/bevyengine/bevy/pull/11838 -[11847]: https://github.com/bevyengine/bevy/pull/11847 -[11850]: https://github.com/bevyengine/bevy/pull/11850 -[11855]: https://github.com/bevyengine/bevy/pull/11855 -[11856]: https://github.com/bevyengine/bevy/pull/11856 -[11860]: https://github.com/bevyengine/bevy/pull/11860 -[11865]: https://github.com/bevyengine/bevy/pull/11865 -[11866]: https://github.com/bevyengine/bevy/pull/11866 -[11867]: https://github.com/bevyengine/bevy/pull/11867 -[11868]: https://github.com/bevyengine/bevy/pull/11868 -[11870]: https://github.com/bevyengine/bevy/pull/11870 -[11878]: https://github.com/bevyengine/bevy/pull/11878 -[11880]: https://github.com/bevyengine/bevy/pull/11880 -[11882]: https://github.com/bevyengine/bevy/pull/11882 -[11884]: https://github.com/bevyengine/bevy/pull/11884 -[11889]: https://github.com/bevyengine/bevy/pull/11889 -[11893]: https://github.com/bevyengine/bevy/pull/11893 -[11894]: https://github.com/bevyengine/bevy/pull/11894 -[11907]: https://github.com/bevyengine/bevy/pull/11907 -[11909]: https://github.com/bevyengine/bevy/pull/11909 -[11910]: https://github.com/bevyengine/bevy/pull/11910 -[11911]: https://github.com/bevyengine/bevy/pull/11911 -[11913]: https://github.com/bevyengine/bevy/pull/11913 -[11914]: https://github.com/bevyengine/bevy/pull/11914 -[11915]: https://github.com/bevyengine/bevy/pull/11915 - -## Version 0.12.0 (2023-11-04) - -### A-ECS + A-Diagnostics - -- [Cache parallel iteration spans][9950] - -### A-ECS + A-Scenes - -- [Make builder types take and return `Self`][10001] - -### A-Scenes - -- [Move scene spawner systems to SpawnScene schedule][9260] -- [Add `SceneInstanceReady`][9313] -- [Add `SpawnScene` to prelude][9451] -- [Finish documenting `bevy_scene`][9949] -- [Only attempt to copy resources that still exist from scenes][9984] -- [Correct Scene loader error description][10161] - -### A-Tasks + A-Diagnostics - -- [Fix doc warning in bevy_tasks][9348] - -### A-Tasks - -- [elaborate on TaskPool and bevy tasks][8750] -- [Remove Resource and add Debug to TaskPoolOptions][9485] -- [Fix clippy lint in single_threaded_task_pool][9851] -- [Remove dependecies from bevy_tasks' README][9881] -- [Allow using async_io::block_on in bevy_tasks][9626] -- [add test for nested scopes][10026] -- [Global TaskPool API improvements][10008] - -### A-Audio + A-Windowing - -- [Application lifetime events (suspend audio on Android)][10158] - -### A-Animation + A-Transform - -- [Add system parameter for computing up-to-date `GlobalTransform`s][8603] - -### A-Transform - -- [Update `GlobalTransform` on insertion][9081] -- [Add `Without` filter to `sync_simple_transforms`' orphaned entities query][9518] -- [Fix ambiguities in transform example][9845] - -### A-App - -- [Add `track_caller` to `App::add_plugins`][9174] -- [Remove redundant check for `AppExit` events in `ScheduleRunnerPlugin`][9421] -- [fix typos in crates/bevy_app/src/app.rs][10173] -- [fix typos in crates/bevy_app/src/app.rs][10173] -- [fix run-once runners][10195] - -### A-ECS + A-App - -- [Add configure_schedules to App and Schedules to apply `ScheduleBuildSettings` to all schedules][9514] -- [Only run event systems if they have tangible work to do][7728] - -### A-Rendering + A-Gizmos - -- [Fix gizmo draw order in 2D][9129] -- [Fix gizmo line width issue when using perspective][9067] - -### A-Rendering + A-Diagnostics - -- [Include note of common profiling issue][9484] -- [Enhance many_cubes stress test use cases][9596] -- [GLTF loader: handle warning NODE_SKINNED_MESH_WITHOUT_SKIN][9360] - -### A-Rendering + A-Reflection - -- [Register `AlphaMode` type][9222] - -### A-Windowing - -- [Add option to toggle window control buttons][9083] -- [Fixed: Default window is now "App" instead of "Bevy App"][9301] -- [improve documentation relating to `WindowPlugin` and `Window`][9173] -- [Improve `bevy_winit` documentation][7609] -- [Change `WinitPlugin` defaults to limit game update rate when window is not visible][7611] -- [User controlled window visibility][9355] -- [Check cursor position for out of bounds of the window][8855] -- [Fix doc link in transparent_window example][9697] -- [Wait before making window visible][9692] -- [don't create windows on winit StartCause::Init event][9684] -- [Fix the doc warning attribute and document remaining items for `bevy_window`][9933] -- [Revert "macOS Sonoma (14.0) / Xcode 15.0 — Compatibility Fixes + Docs…][9991] -- [Revert "macOS Sonoma (14.0) / Xcode 15.0 — Compatibility Fixes + Docs…][9991] -- [Allow Bevy to start from non-main threads on supported platforms][10020] -- [Prevent black frames during startup][9826] -- [Slightly improve `CursorIcon` doc.][10289] -- [Fix typo in window.rs][10358] - -### A-Gizmos - -- [Replace AHash with a good sequence for entity AABB colors][9175] -- [gizmo plugin lag bugfix][9166] -- [Clarify immediate mode in `Gizmos` documentation][9183] -- [Fix crash when drawing line gizmo with less than 2 vertices][9101] -- [Document that gizmo `depth_bias` has no effect in 2D][10074] - -### A-Utils - -- [change 'collapse_type_name' to retain enum types][9587] -- [bevy_derive: Fix `#[deref]` breaking other attributes][9551] -- [Move default docs][9638] - -### A-Rendering + A-Assets - -- [Import the second UV map if present in glTF files.][9992] -- [fix custom shader imports][10030] -- [Add `ImageSamplerDescriptor` as an image loader setting][9982] - -### A-ECS - -- [Add the Has world query to bevy_ecs::prelude][9204] -- [Simplify parallel iteration methods][8854] -- [Fix safety invariants for `WorldQuery::fetch` and simplify cloning][8246] -- [Derive debug for ManualEventIterator][9293] -- [Add `EntityMap::clear`][9291] -- [Add a paragraph to the lifetimeless module doc][9312] -- [opt-out `multi-threaded` feature flag][9269] -- [Fix `ambiguous_with` breaking run conditions][9253] -- [Add `RunSystem`][9366] -- [Add `replace_if_neq` to `DetectChangesMut`][9418] -- [Adding `Copy, Clone, Debug` to derived traits of `ExecutorKind`][9385] -- [Fix incorrect documentation link in `DetectChangesMut`][9431] -- [Implement `Debug` for `UnsafeWorldCell`][9460] -- [Relax In/Out bounds on impl Debug for dyn System][9581] -- [Improve various `Debug` implementations][9588] -- [Make `run_if_inner` public and rename to `run_if_dyn`][9576] -- [Refactor build_schedule and related errors][9579] -- [Add `system.map(...)` for transforming the output of a system][8526] -- [Reorganize `Events` and `EventSequence` code][9306] -- [Replaced EntityMap with HashMap][9461] -- [clean up configure_set(s) erroring][9577] -- [Relax more `Sync` bounds on `Local`][9589] -- [Rename `ManualEventIterator`][9592] -- [Replaced `EntityCommand` Implementation for `FnOnce`][9604] -- [Add a variant of `Events::update` that returns the removed events][9542] -- [Move schedule name into `Schedule`][9600] -- [port old ambiguity tests over][9617] -- [Refactor `EventReader::iter` to `read`][9631] -- [fix ambiguity reporting][9648] -- [Fix anonymous set name stack overflow][9650] -- [Fix unsoundness in `QueryState::is_empty`][9463] -- [Add panicking helpers for getting components from `Query`][9659] -- [Replace `IntoSystemSetConfig` with `IntoSystemSetConfigs`][9247] -- [Moved `get_component(_unchecked_mut)` from `Query` to `QueryState`][9686] -- [Fix naming on "tick" Column and ComponentSparseSet methods][9744] -- [Clarify a comment in Option WorldQuery impl][9749] -- [Return a boolean from `set_if_neq`][9801] -- [Rename RemovedComponents::iter/iter_with_id to read/read_with_id][9778] -- [Remove some old references to CoreSet][9833] -- [Use single threaded executor for archetype benches][9835] -- [docs: Improve some `ComponentId` doc cross-linking.][9839] -- [One Shot Systems][8963] -- [Add mutual exclusion safety info on filter_fetch][9836] -- [add try_insert to entity commands][9844] -- [Improve codegen for world validation][9464] -- [docs: Use intradoc links for method references.][9958] -- [Remove States::variants and remove enum-only restriction its derive][9945] -- [`as_deref_mut()` method for Mut-like types][9912] -- [refactor: Change `Option>` query params to `Has`][9959] -- [Hide `UnsafeWorldCell::unsafe_world`][9741] -- [Add a public API to ArchetypeGeneration/Id][9825] -- [Ignore ambiguous components or resources][9895] -- [Use chain in breakout example][10124] -- [`ParamSet`s containing non-send parameters should also be non-send][10211] -- [Replace all labels with interned labels][7762] -- [Fix outdated comment referencing CoreSet][10294] - -### A-Rendering + A-Math - -- [derive Clone/Copy/Debug trio for shape::Cylinder][9705] - -### A-UI - -- [Fix for vertical text bounds and alignment][9133] -- [UI extraction order fix][9099] -- [Update text example using default font][9259] -- [bevy_ui: fix doc formatting for some Style fields][9295] -- [Remove the `With` query filter from `bevy_ui::render::extract_uinode_borders`][9285] -- [Fix incorrent doc comment for the set method of `ContentSize`][9345] -- [Improved text widget doc comments][9344] -- [Change the default for the `measure_func` field of `ContentSize` to None.][9346] -- [Unnecessary line in game_menu example][9406] -- [Change `UiScale` to a tuple struct][9444] -- [Remove unnecessary doc string][9481] -- [Add some missing pub in ui_node][9529] -- [UI examples clean up][9479] -- [`round_ties_up` fix][9548] -- [fix incorrect docs for `JustifyItems` and `JustifySelf`][9539] -- [Added `Val::ZERO` Constant][9566] -- [Cleanup some bevy_text pipeline.rs][9111] -- [Make `GridPlacement`'s fields non-zero and add accessor functions.][9486] -- [Remove `Val`'s `try_*` arithmetic methods][9609] -- [UI node bundle comment fix][9404] -- [Do not panic on non-UI child of UI entity][9621] -- [Rename `Val` `evaluate` to `resolve` and implement viewport variant support][9568] -- [Change `Urect::width` & `Urect::height` to be const][9640] -- [`TextLayoutInfo::size` should hold the drawn size of the text, and not a scaled value.][7794] -- [`impl From` and `From<&str>` for `TextSection`][8856] -- [Remove z-axis scaling in `extract_text2d_sprite`][9733] -- [Fix doc comments for align items][9739] -- [Add tests to `bevy_ui::Layout`][9781] -- [examples: Remove unused doc comments.][9795] -- [Add missing `bevy_text` feature attribute to `TextBundle` from impl][9785] -- [Move `Val` into `geometry`][9818] -- [Derive Serialize and Deserialize for UiRect][9820] -- [`ContentSize` replacement fix][9753] -- [Round UI coordinates after scaling][9784] -- [Have a separate implicit viewport node per root node + make viewport node `Display::Grid`][9637] -- [Rename `num_font_atlases` to `len`.][9879] -- [Fix documentation for ui node Style][9935] -- [`text_wrap_debug` scale factor commandline args][9951] -- [Store both the rounded and unrounded node size in Node][9923] -- [Various accessibility API updates.][9989] -- [UI node outlines][9931] -- [Implement serialize and deserialize for some UI types][10044] -- [Tidy up UI node docs][10189] -- [Remove unused import warning when default_font feature is disabled][10230] -- [Fix crash with certain right-aligned text][10271] -- [Add some more docs for bevy_text.][9873] -- [Implement `Neg` for `Val`][10295] -- [`normalize` method for `Rect`][10297] -- [don't Implement `Display` for `Val`][10345] -- [[bevy_text] Document what happens when font is not specified][10252] -- [Update UI alignment docs][10303] -- [Add stack index to `Node`][9853] -- [don't Implement `Display` for `Val`][10345] - -### A-Animation - -- [Fix doc typo][9162] -- [Expose `animation_clip` paths][9392] -- [animations: convert skinning weights from unorm8x4 to float32x4][9338] -- [API updates to the AnimationPlayer][9002] -- [only take up to the max number of joints][9351] -- [check root node for animations][9407] -- [Fix morph interpolation][9927] - -### A-Pointers - -- [Put `#[repr(transparent)]` attr to bevy_ptr types][9068] - -### A-Assets + A-Reflection - -- [reflect: `TypePath` part 2][8768] - -### A-Rendering + A-Hierarchy - -- [default inherited visibility when parent has invalid components][10275] - -### A-ECS + A-Tasks - -- [Round up for the batch size to improve par_iter performance][9814] - -### A-Reflection + A-Utils - -- [Moved `fq_std` from `bevy_reflect_derive` to `bevy_macro_utils`][9956] - -### A-Reflection + A-Math - -- [Add reflect impls to IRect and URect][9191] -- [Implement reflect trait on new glam types (I64Vec and U64Vec)][9281] - -### A-Hierarchy - -- [Prevent setting parent as itself][8980] -- [Add as_slice to parent][9871] - -### A-Input - -- [input: allow multiple gamepad inputs to be registered for one button in one frame][9446] -- [Bevy Input Docs : lib.rs][9468] -- [Bevy Input Docs : gamepad.rs][9469] -- [Add `GamepadButtonInput` event][9008] -- [Bevy Input Docs : the modules][9467] -- [Finish documenting `bevy_gilrs`][10010] -- [Change `AxisSettings` livezone default][10090] -- [docs: Update input_toggle_active example][9913] - -### A-Input + A-Windowing - -- [Fix `Window::set_cursor_position`][9456] -- [Change `Window::physical_cursor_position` to use the physical size of the window][9657] -- [Fix check that cursor position is within window bounds][9662] - -### A-ECS + A-Reflection - -- [implement insert and remove reflected entity commands][8895] -- [Allow disjoint mutable world access via `EntityMut`][9419] -- [Implement `Reflect` for `State` and `NextState`][9742] -- [`#[derive(Clone)]` on `Component{Info,Descriptor}`][9812] - -### A-Math - -- [Rename bevy_math::rects conversion methods][9159] -- [Add glam swizzles traits to prelude][9387] -- [Rename `Bezier` to `CubicBezier` for clarity][9554] -- [Add a method to compute a bounding box enclosing a set of points][9630] -- [re-export `debug_glam_assert` feature][10206] -- [Add `Cubic` prefix to all cubic curve generators][10299] - -### A-Build-System - -- [only check for bans if the dependency tree changed][9252] -- [Slightly better message when contributor modifies examples template][9372] -- [switch CI jobs between windows and linux for example execution][9489] -- [Check for bevy_internal imports in CI][9612] -- [Fix running examples on linux in CI][9665] -- [Bump actions/checkout from 2 to 4][9759] -- [doc: Remove reference to `clippy::manual-strip`.][9794] -- [Only run some workflows on the bevy repo (not forks)][9872] -- [run mobile tests on more devices / OS versions][9936] -- [Allow `clippy::type_complexity` in more places.][9796] -- [hacks for running (and screenshotting) the examples in CI on a github runner][9220] -- [make CI less failing on cargo deny bans][10151] -- [add test on Android 14 / Pixel 8][10148] -- [Use `clippy::doc_markdown` more.][10286] - -### A-Diagnostics - -- [Cache System Tracing Spans][9390] - -### A-Rendering + A-Animation - -- [Use a seeded rng for custom_skinned_mesh example][9846] -- [Move skin code to a separate module][9899] - -### A-Core - -- [Change visibility of `bevy::core::update_frame_count` to `pub`][10111] - -### A-Reflection - -- [Fix typo in NamedTypePathDef][9102] -- [Refactor `path` module of `bevy_reflect`][8887] -- [Refactor parsing in bevy_reflect path module][9048] -- [bevy_reflect: Fix combined field attributes][9322] -- [bevy_reflect: Opt-out attribute for `TypePath`][9140] -- [Add reflect path parsing benchmark][9364] -- [Make it so `ParsedPath` can be passed to GetPath][9373] -- [Make the reflect path parser utf-8-unaware][9371] -- [bevy_scene: Add `ReflectBundle`][9165] -- [Fix comment in scene example `FromResources`][9743] -- [Remove TypeRegistry re-export rename][9807] -- [Provide getters for fields of ReflectFromPtr][9748] -- [Add TypePath to the prelude][9963] -- [Improve TypeUuid's derive macro error messages][9315] -- [Migrate `Quat` reflection strategy from "value" to "struct"][10068] -- [bevy_reflect: Fix dynamic type serialization][10103] -- [bevy_reflect: Fix ignored/skipped field order][7575] - -### A-Rendering + A-Assets + A-Reflection - -- [Implement `Reflect` for `Mesh`][9779] - -### A-ECS + A-Time - -- [add on_real_time_timer run condition][10179] - -### A-ECS + A-Hierarchy - -- [Added 'clear_children' and 'replace_children' methods to BuildWorldChildren to be consistent with BuildChildren.][10311] - -### A-Audio - -- [Added Pitch as an alternative sound source][9225] -- [update documentation on AudioSink][9332] -- [audio sinks don't need their custom drop anymore][9336] -- [Clarify what happens when setting the audio volume][9480] -- [More ergonomic spatial audio][9800] - -### A-Rendering + A-UI - -- [Remove out-of-date paragraph in `Style::border`][9103] -- [Revert "Fix UI corruption for AMD gpus with Vulkan (#9169)"][9237] -- [Revert "Fix UI corruption for AMD gpus with Vulkan (#9169)"][9237] -- [`many_buttons` enhancements][9712] -- [Fix UI borders][10078] -- [UI batching Fix][9610] -- [Add UI Materials][9506] - -### A-ECS + A-Reflection + A-Pointers - -- [add `MutUntyped::map_unchanged`][9194] - -### No area label - -- [Fix typos throughout the project][9090] -- [Bump Version after Release][9106] -- [fix `clippy::default_constructed_unit_structs` and trybuild errors][9144] -- [delete code deprecated in 0.11][9128] -- [Drain `ExtractedUiNodes` in `prepare_uinodes`][9142] -- [example showcase - pagination and can build for WebGL2][9168] -- [example showcase: switch default api to webgpu][9193] -- [Add some more helpful errors to BevyManifest when it doesn't find Cargo.toml][9207] -- [Fix path reference to contributors example][9219] -- [replace parens with square brackets when referencing _mut on `Query` docs #9200][9223] -- [use AutoNoVsync in stress tests][9229] -- [bevy_render: Remove direct dep on wgpu-hal.][9249] -- [Fixed typo in line 322][9276] -- [custom_material.vert: gl_InstanceIndex includes gl_BaseInstance][9326] -- [fix typo in a link - Mesh docs][9329] -- [Improve font size related docs][9320] -- [Fix gamepad viewer being marked as a non-wasm example][9399] -- [Rustdoc: Scrape examples][9154] -- [enable multithreading on benches][9388] -- [webgl feature renamed to webgl2][9370] -- [Example Comment Typo Fix][9427] -- [Fix shader_instancing example][9448] -- [Update tracy-client requirement from 0.15 to 0.16][9436] -- [fix bevy imports. windows_settings.rs example][9547] -- [Fix CI for Rust 1.72][9562] -- [Swap TransparentUi to use a stable sort][9598] -- [Replace uses of `entity.insert` with tuple bundles in `game_menu` example][9619] -- [Remove `IntoIterator` impl for `&mut EventReader`][9583] -- [remove VecSwizzles imports][9629] -- [Fix erronenous glam version][9653] -- [Fixing some doc comments][9646] -- [Explicitly make instance_index vertex output @interpolate(flat)][9675] -- [Fix some nightly warnings][9672] -- [Use default resolution for viewport_debug example][9666] -- [Refer to "macOS", not "macOS X".][9704] -- [Remove useless single tuples and trailing commas][9720] -- [Fix some warnings shown in nightly][10012] -- [Fix animate_scale scaling z value in text2d example][9769] -- ["serialize" feature no longer enables the optional "bevy_scene" feature if it's not enabled from elsewhere][9803] -- [fix deprecation warning in bench][9823] -- [don't enable filesystem_watcher when building for WebGPU][9829] -- [Improve doc formatting.][9840] -- [Fix the `clippy::explicit_iter_loop` lint][9834] -- [Wslg docs][9842] -- [skybox.wgsl: Fix precision issues][9909] -- [Fix typos.][9922] -- [Add link to `Text2dBundle` in `TextBundle` docs.][9900] -- [Fix some typos][9934] -- [Fix typos][9965] -- [Replaced `parking_lot` with `std::sync`][9545] -- [Add inline(never) to bench systems][9824] -- [Android: handle suspend / resume][9937] -- [Fix some warnings shown in nightly][10012] -- [Updates for rust 1.73][10035] -- [Improve selection of iOS device in mobile example][9282] -- [Update toml_edit requirement from 0.19 to 0.20][10058] -- [foxes shouldn't march in sync][10070] -- [Fix tonemapping test patten][10092] -- [Removed `once_cell`][10079] -- [Improve WebGPU unstable flags docs][10163] -- [shadow_biases: Support different PCF methods][10184] -- [shadow_biases: Support moving the light position and resetting biases][10185] -- [Update async-io requirement from 1.13.0 to 2.0.0][10238] -- [few fmt tweaks][10264] -- [Derive Error for more error types][10240] -- [Allow AccessKit to react to WindowEvents before they reach the engine][10356] - -### A-Rendering + A-Build-System - -- [Improve execution of examples in CI][9331] -- [make deferred_rendering simpler to render for CI][10150] - -### A-Meta - -- [Remove the bevy_dylib feature][9516] -- [add and fix shields in Readmes][9993] -- [Added section for contributing and links for issues and PRs][10171] -- [Fix orphaned contributing paragraph][10174] - -### A-Assets + A-Animation - -- [Handle empty morph weights when loading gltf][9867] -- [Finish documenting `bevy_gltf`][9998] - -### A-Editor + A-Diagnostics - -- [Add `DiagnosticsStore::iter_mut`][9679] - -### A-Time - -- [Fix timers.rs documentation][9290] -- [Add missing documentation to `bevy_time`][9428] -- [Clarify behaviour of `Timer::finished()` for repeating timers][9939] -- [ignore time channel error][9981] -- [Unify `FixedTime` and `Time` while fixing several problems][8964] -- [Time: demote delta time clamping warning to debug][10145] -- [fix typo in time.rs example][10152] -- [Example time api][10204] - -### A-Rendering + A-ECS - -- [Update `Camera`'s `Frustum` only when its `GlobalTransform` or `CameraProjection` changed][9092] - -### A-UI + A-Reflection - -- [bevy_ui: reflect missing types][9677] -- [register `TextLayoutInfo` and `TextFlags` type.][9919] - -### A-Build-System + A-Assets - -- [Increase iteration count for asset tests][9737] - -### A-Rendering - -- [Clarify that wgpu is based on the webGPU API][9093] -- [Return URect instead of (UVec2, UVec2) in Camera::physical_viewport_rect][9085] -- [fix module name for AssetPath shaders][9186] -- [Add GpuArrayBuffer and BatchedUniformBuffer][8204] -- [Update `bevy_window::PresentMode` to mirror `wgpu::PresentMode`][9230] -- [Stop using unwrap in the pipelined rendering thread][9052] -- [Fix panic whilst loading UASTC encoded ktx2 textures][9158] -- [Document `ClearColorConfig`][9288] -- [Use GpuArrayBuffer for MeshUniform][9254] -- [Update docs for scaling_mode field of Orthographic projection][9297] -- [Fix shader_material_glsl example after #9254][9311] -- [Improve `Mesh` documentation][9061] -- [Include tone_mapping fn in tonemapping_test_patterns][9084] -- [Extend the default render range of 2D camera][9310] -- [Document when Camera::viewport_to_world and related methods return None][8841] -- [include toplevel shader-associated defs][9343] -- [Fix post_processing example on webgl2][9361] -- [use ViewNodeRunner in the post_processing example][9127] -- [Work around naga/wgpu WGSL instance_index -> GLSL gl_InstanceID bug on WebGL2][9383] -- [Fix non-visible motion vector text in shader prepass example][9155] -- [Use bevy crates imports instead of bevy internal. post_processing example][9396] -- [Make Anchor Copy][9327] -- [Move window.rs to window/mod.rs in bevy_render][9394] -- [Reduce the size of MeshUniform to improve performance][9416] -- [Fix temporal jitter bug][9462] -- [Fix gizmo lines deforming or disappearing when partially behind the camera][9470] -- [Make WgpuSettings::default() check WGPU_POWER_PREF][9482] -- [fix wireframe after MeshUniform size reduction][9505] -- [fix shader_material_glsl example][9513] -- [[RAINBOW EFFECT] Added methods to get HSL components from Color][9201] -- [ktx2: Fix Rgb8 -> Rgba8Unorm conversion][9555] -- [Reorder render sets, refactor bevy_sprite to take advantage][9236] -- [Improve documentation relating to `Frustum` and `HalfSpace`][9136] -- [Revert "Update defaults for OrthographicProjection (#9537)"][9878] -- [Remove unused regex dep from bevy_render][9613] -- [Split `ComputedVisibility` into two components to allow for accurate change detection and speed up visibility propagation][9497] -- [Use instancing for sprites][9597] -- [Enhance bevymark][9674] -- [Remove redundant math in tonemapping.][9669] -- [Improve `SpatialBundle` docs][9673] -- [Cache depth texture based on usage][9565] -- [warn and min for different vertex count][9699] -- [default 16bit rgb/rgba textures to unorm instead of uint][9611] -- [Fix TextureAtlasBuilder padding][10031] -- [Add example for `Camera::viewport_to_world`][7179] -- [Fix wireframe for skinned/morphed meshes][9734] -- [generate indices for Mikktspace][8862] -- [invert face culling for negatively scaled gltf nodes][8859] -- [renderer init: create a detached task only on wasm, block otherwise][9830] -- [Cleanup `visibility` module][9850] -- [Use a single line for of large binding lists][9849] -- [Fix a typo in `DirectionalLightBundle`][9861] -- [Revert "Update defaults for OrthographicProjection (#9537)"][9878] -- [Refactor rendering systems to use `let-else`][9870] -- [Use radsort for Transparent2d PhaseItem sorting][9882] -- [Automatic batching/instancing of draw commands][9685] -- [Directly copy data into uniform buffers][9865] -- [Allow other plugins to create renderer resources][9925] -- [Use EntityHashMap for render world entity storage for better performance][9903] -- [Parallelize extract_meshes][9966] -- [Fix comment grammar][9990] -- [Allow overriding global wireframe setting.][7328] -- [wireframes: workaround for DX12][10022] -- [Alternate wireframe override api][10023] -- [Fix TextureAtlasBuilder padding][10031] -- [fix example mesh2d_manual][9941] -- [PCF For DirectionalLight/SpotLight Shadows][8006] -- [Refactor the render instance logic in #9903 so that it's easier for other components to adopt.][10002] -- [Fix 2d_shapes and general 2D mesh instancing][10051] -- [fix webgl2 crash][10053] -- [fix orthographic cluster aabb for spotlight culling][9614] -- [Add consuming builder methods for more ergonomic `Mesh` creation][10056] -- [wgpu 0.17][9302] -- [use `Material` for wireframes][5314] -- [Extract common wireframe filters in type alias][10080] -- [Deferred Renderer][9258] -- [Configurable colors for wireframe][5303] -- [chore: Renamed RenderInstance trait to ExtractInstance][10065] -- [pbr shader cleanup][10105] -- [Fix text2d view-visibility][10100] -- [Allow optional extraction of resources from the main world][10109] -- [ssao use unlit_color instead of white][10117] -- [Fix missing explicit lifetime name for copy_deferred_lighting_id name][10128] -- [Fixed mod.rs in rendering to support Radeon Cards][10132] -- [Explain usage of prepass shaders in docs for `Material` trait][9025] -- [Better link for prepare_windows docs][10142] -- [Improve linking within `RenderSet` docs.][10143] -- [Fix unlit missing parameters][10144] -- [`*_PREPASS` Shader Def Cleanup][10136] -- [check for any prepass phase][10160] -- [allow extensions to StandardMaterial][7820] -- [array_texture example: use new name of pbr function][10168] -- [chore: use ExtractComponent derive macro for EnvironmentMapLight and FogSettings][10191] -- [Variable `MeshPipeline` View Bind Group Layout][10156] -- [update shader imports][10180] -- [Bind group entries][9694] -- [Detect cubemap for dds textures][10222] -- [Fix alignment on ios simulator][10178] -- [Add convenient methods for Image][10221] -- [Use “specular occlusion” term to consistently extinguish fresnel on Ambient and Environment Map lights][10182] -- [Fix fog color being inaccurate][10226] -- [Replace all usages of texture_descritor.size.* with the helper methods][10227] -- [View Transformations][9726] -- [fix deferred example fog values][10249] -- [WebGL2: fix import path for unpack_unorm3x4_plus_unorm_20_][10251] -- [Use wildcard imports in bevy_pbr][9847] -- [Make mesh attr vertex count mismatch warn more readable][10259] -- [Image Sampler Improvements][10254] -- [Fix sampling of diffuse env map texture with non-uniform control flow][10276] -- [Log a warning when the `tonemapping_luts` feature is disabled but required for the selected tonemapper.][10253] -- [Smaller TAA fixes][10200] -- [Truncate attribute buffer data rather than attribute buffers][10270] -- [Fix deferred lighting pass values not all working on M1 in WebGL2][10304] -- [Add frustum to shader View][10306] -- [Fix handling of `double_sided` for normal maps][10326] -- [Add helper function to determine if color is transparent][10310] -- [`StandardMaterial` Light Transmission][8015] -- [double sided normals: fix apply_normal_mapping calls][10330] -- [Combine visibility queries in check_visibility_system][10196] -- [Make VERTEX_COLORS usable in prepass shader, if available][10341] -- [allow DeferredPrepass to work without other prepass markers][10223] -- [Increase default normal bias to avoid common artifacts][10346] -- [Make `DirectionalLight` `Cascades` computation generic over `CameraProjection`][9226] -- [Update default `ClearColor` to better match Bevy's branding][10339] -- [Fix gizmo crash when prepass enabled][10360] - -### A-Build-System + A-Meta - -- [Fixed: README.md][9994] - -### A-Assets - -- [doc(asset): fix asset trait example][9105] -- [Add `GltfLoader::new`.][9120] -- [impl `From<&AssetPath>` for `HandleId`][9132] -- [allow asset loader pre-registration][9429] -- [fix asset loader preregistration for multiple assets][9453] -- [Fix point light radius][9493] -- [Add support for KHR_materials_emissive_strength][9553] -- [Fix panic when using `.load_folder()` with absolute paths][9490] -- [Bevy Asset V2][8624] -- [create imported asset directory if needed][9716] -- [Copy on Write AssetPaths][9729] -- [Asset v2: Asset path serialization fix][9756] -- [don't ignore some EventKind::Modify][9767] -- [Manual "Reflect Value" AssetPath impl to fix dynamic linking][9752] -- [Fix unused variable warning for simple AssetV2 derives][9961] -- [Remove monkey.gltf][9974] -- [Update notify-debouncer-full requirement from 0.2.0 to 0.3.1][9757] -- [Removed `anyhow`][10003] -- [Multiple Asset Sources][9885] -- [Make loading warning for no file ext more descriptive][10119] -- [Fix load_folder for non-default Asset Sources][10121] -- [only set up processed source if asset plugin is not unprocessed][10123] -- [Hot reload labeled assets whose source asset is not loaded][9736] -- [Return an error when loading non-existent labels][9751] -- [remove unused import on android][10197] -- [Log an error when registering an AssetSource after AssetPlugin has been built][10202] -- [Add note about asset source register order][10186] -- [Add `asset_processor` feature and remove AssetMode::ProcessedDev][10194] -- [Implement source into Display for AssetPath][10217] -- [assets: use blake3 instead of md5][10208] -- [Reduce noise in asset processing example][10262] -- [Adding AssetPath::resolve() method.][9528] -- [Assets: fix first hot reloading][9804] -- [Non-blocking load_untyped using a wrapper asset][10198] -- [Reuse and hot reload folder handles][10210] -- [Additional AssetPath unit tests.][10279] -- [Corrected incorrect doc comment on read_asset_bytes][10352] -- [support file operations in single threaded context][10312] - -[5303]: https://github.com/bevyengine/bevy/pull/5303 -[5314]: https://github.com/bevyengine/bevy/pull/5314 -[7179]: https://github.com/bevyengine/bevy/pull/7179 -[7328]: https://github.com/bevyengine/bevy/pull/7328 -[7575]: https://github.com/bevyengine/bevy/pull/7575 -[7609]: https://github.com/bevyengine/bevy/pull/7609 -[7611]: https://github.com/bevyengine/bevy/pull/7611 -[7728]: https://github.com/bevyengine/bevy/pull/7728 -[7762]: https://github.com/bevyengine/bevy/pull/7762 -[7794]: https://github.com/bevyengine/bevy/pull/7794 -[7820]: https://github.com/bevyengine/bevy/pull/7820 -[8006]: https://github.com/bevyengine/bevy/pull/8006 -[8015]: https://github.com/bevyengine/bevy/pull/8015 -[8204]: https://github.com/bevyengine/bevy/pull/8204 -[8246]: https://github.com/bevyengine/bevy/pull/8246 -[8526]: https://github.com/bevyengine/bevy/pull/8526 -[8603]: https://github.com/bevyengine/bevy/pull/8603 -[8624]: https://github.com/bevyengine/bevy/pull/8624 -[8750]: https://github.com/bevyengine/bevy/pull/8750 -[8768]: https://github.com/bevyengine/bevy/pull/8768 -[8841]: https://github.com/bevyengine/bevy/pull/8841 -[8854]: https://github.com/bevyengine/bevy/pull/8854 -[8855]: https://github.com/bevyengine/bevy/pull/8855 -[8856]: https://github.com/bevyengine/bevy/pull/8856 -[8859]: https://github.com/bevyengine/bevy/pull/8859 -[8862]: https://github.com/bevyengine/bevy/pull/8862 -[8887]: https://github.com/bevyengine/bevy/pull/8887 -[8895]: https://github.com/bevyengine/bevy/pull/8895 -[8963]: https://github.com/bevyengine/bevy/pull/8963 -[8964]: https://github.com/bevyengine/bevy/pull/8964 -[8980]: https://github.com/bevyengine/bevy/pull/8980 -[9002]: https://github.com/bevyengine/bevy/pull/9002 -[9008]: https://github.com/bevyengine/bevy/pull/9008 -[9025]: https://github.com/bevyengine/bevy/pull/9025 -[9048]: https://github.com/bevyengine/bevy/pull/9048 -[9052]: https://github.com/bevyengine/bevy/pull/9052 -[9061]: https://github.com/bevyengine/bevy/pull/9061 -[9067]: https://github.com/bevyengine/bevy/pull/9067 -[9068]: https://github.com/bevyengine/bevy/pull/9068 -[9081]: https://github.com/bevyengine/bevy/pull/9081 -[9083]: https://github.com/bevyengine/bevy/pull/9083 -[9084]: https://github.com/bevyengine/bevy/pull/9084 -[9085]: https://github.com/bevyengine/bevy/pull/9085 -[9090]: https://github.com/bevyengine/bevy/pull/9090 -[9092]: https://github.com/bevyengine/bevy/pull/9092 -[9093]: https://github.com/bevyengine/bevy/pull/9093 -[9099]: https://github.com/bevyengine/bevy/pull/9099 -[9101]: https://github.com/bevyengine/bevy/pull/9101 -[9102]: https://github.com/bevyengine/bevy/pull/9102 -[9103]: https://github.com/bevyengine/bevy/pull/9103 -[9105]: https://github.com/bevyengine/bevy/pull/9105 -[9106]: https://github.com/bevyengine/bevy/pull/9106 -[9111]: https://github.com/bevyengine/bevy/pull/9111 -[9120]: https://github.com/bevyengine/bevy/pull/9120 -[9127]: https://github.com/bevyengine/bevy/pull/9127 -[9128]: https://github.com/bevyengine/bevy/pull/9128 -[9129]: https://github.com/bevyengine/bevy/pull/9129 -[9132]: https://github.com/bevyengine/bevy/pull/9132 -[9133]: https://github.com/bevyengine/bevy/pull/9133 -[9136]: https://github.com/bevyengine/bevy/pull/9136 -[9140]: https://github.com/bevyengine/bevy/pull/9140 -[9142]: https://github.com/bevyengine/bevy/pull/9142 -[9144]: https://github.com/bevyengine/bevy/pull/9144 -[9154]: https://github.com/bevyengine/bevy/pull/9154 -[9155]: https://github.com/bevyengine/bevy/pull/9155 -[9158]: https://github.com/bevyengine/bevy/pull/9158 -[9159]: https://github.com/bevyengine/bevy/pull/9159 -[9162]: https://github.com/bevyengine/bevy/pull/9162 -[9165]: https://github.com/bevyengine/bevy/pull/9165 -[9166]: https://github.com/bevyengine/bevy/pull/9166 -[9168]: https://github.com/bevyengine/bevy/pull/9168 -[9173]: https://github.com/bevyengine/bevy/pull/9173 -[9174]: https://github.com/bevyengine/bevy/pull/9174 -[9175]: https://github.com/bevyengine/bevy/pull/9175 -[9183]: https://github.com/bevyengine/bevy/pull/9183 -[9186]: https://github.com/bevyengine/bevy/pull/9186 -[9191]: https://github.com/bevyengine/bevy/pull/9191 -[9193]: https://github.com/bevyengine/bevy/pull/9193 -[9194]: https://github.com/bevyengine/bevy/pull/9194 -[9201]: https://github.com/bevyengine/bevy/pull/9201 -[9204]: https://github.com/bevyengine/bevy/pull/9204 -[9207]: https://github.com/bevyengine/bevy/pull/9207 -[9219]: https://github.com/bevyengine/bevy/pull/9219 -[9220]: https://github.com/bevyengine/bevy/pull/9220 -[9222]: https://github.com/bevyengine/bevy/pull/9222 -[9223]: https://github.com/bevyengine/bevy/pull/9223 -[9225]: https://github.com/bevyengine/bevy/pull/9225 -[9226]: https://github.com/bevyengine/bevy/pull/9226 -[9229]: https://github.com/bevyengine/bevy/pull/9229 -[9230]: https://github.com/bevyengine/bevy/pull/9230 -[9236]: https://github.com/bevyengine/bevy/pull/9236 -[9237]: https://github.com/bevyengine/bevy/pull/9237 -[9247]: https://github.com/bevyengine/bevy/pull/9247 -[9249]: https://github.com/bevyengine/bevy/pull/9249 -[9252]: https://github.com/bevyengine/bevy/pull/9252 -[9253]: https://github.com/bevyengine/bevy/pull/9253 -[9254]: https://github.com/bevyengine/bevy/pull/9254 -[9258]: https://github.com/bevyengine/bevy/pull/9258 -[9259]: https://github.com/bevyengine/bevy/pull/9259 -[9260]: https://github.com/bevyengine/bevy/pull/9260 -[9269]: https://github.com/bevyengine/bevy/pull/9269 -[9276]: https://github.com/bevyengine/bevy/pull/9276 -[9281]: https://github.com/bevyengine/bevy/pull/9281 -[9282]: https://github.com/bevyengine/bevy/pull/9282 -[9285]: https://github.com/bevyengine/bevy/pull/9285 -[9288]: https://github.com/bevyengine/bevy/pull/9288 -[9290]: https://github.com/bevyengine/bevy/pull/9290 -[9291]: https://github.com/bevyengine/bevy/pull/9291 -[9293]: https://github.com/bevyengine/bevy/pull/9293 -[9295]: https://github.com/bevyengine/bevy/pull/9295 -[9297]: https://github.com/bevyengine/bevy/pull/9297 -[9301]: https://github.com/bevyengine/bevy/pull/9301 -[9302]: https://github.com/bevyengine/bevy/pull/9302 -[9306]: https://github.com/bevyengine/bevy/pull/9306 -[9310]: https://github.com/bevyengine/bevy/pull/9310 -[9311]: https://github.com/bevyengine/bevy/pull/9311 -[9312]: https://github.com/bevyengine/bevy/pull/9312 -[9313]: https://github.com/bevyengine/bevy/pull/9313 -[9315]: https://github.com/bevyengine/bevy/pull/9315 -[9320]: https://github.com/bevyengine/bevy/pull/9320 -[9322]: https://github.com/bevyengine/bevy/pull/9322 -[9326]: https://github.com/bevyengine/bevy/pull/9326 -[9327]: https://github.com/bevyengine/bevy/pull/9327 -[9329]: https://github.com/bevyengine/bevy/pull/9329 -[9331]: https://github.com/bevyengine/bevy/pull/9331 -[9332]: https://github.com/bevyengine/bevy/pull/9332 -[9336]: https://github.com/bevyengine/bevy/pull/9336 -[9338]: https://github.com/bevyengine/bevy/pull/9338 -[9343]: https://github.com/bevyengine/bevy/pull/9343 -[9344]: https://github.com/bevyengine/bevy/pull/9344 -[9345]: https://github.com/bevyengine/bevy/pull/9345 -[9346]: https://github.com/bevyengine/bevy/pull/9346 -[9348]: https://github.com/bevyengine/bevy/pull/9348 -[9351]: https://github.com/bevyengine/bevy/pull/9351 -[9355]: https://github.com/bevyengine/bevy/pull/9355 -[9360]: https://github.com/bevyengine/bevy/pull/9360 -[9361]: https://github.com/bevyengine/bevy/pull/9361 -[9364]: https://github.com/bevyengine/bevy/pull/9364 -[9366]: https://github.com/bevyengine/bevy/pull/9366 -[9370]: https://github.com/bevyengine/bevy/pull/9370 -[9371]: https://github.com/bevyengine/bevy/pull/9371 -[9372]: https://github.com/bevyengine/bevy/pull/9372 -[9373]: https://github.com/bevyengine/bevy/pull/9373 -[9383]: https://github.com/bevyengine/bevy/pull/9383 -[9385]: https://github.com/bevyengine/bevy/pull/9385 -[9387]: https://github.com/bevyengine/bevy/pull/9387 -[9388]: https://github.com/bevyengine/bevy/pull/9388 -[9390]: https://github.com/bevyengine/bevy/pull/9390 -[9392]: https://github.com/bevyengine/bevy/pull/9392 -[9394]: https://github.com/bevyengine/bevy/pull/9394 -[9396]: https://github.com/bevyengine/bevy/pull/9396 -[9399]: https://github.com/bevyengine/bevy/pull/9399 -[9404]: https://github.com/bevyengine/bevy/pull/9404 -[9406]: https://github.com/bevyengine/bevy/pull/9406 -[9407]: https://github.com/bevyengine/bevy/pull/9407 -[9416]: https://github.com/bevyengine/bevy/pull/9416 -[9418]: https://github.com/bevyengine/bevy/pull/9418 -[9419]: https://github.com/bevyengine/bevy/pull/9419 -[9421]: https://github.com/bevyengine/bevy/pull/9421 -[9427]: https://github.com/bevyengine/bevy/pull/9427 -[9428]: https://github.com/bevyengine/bevy/pull/9428 -[9429]: https://github.com/bevyengine/bevy/pull/9429 -[9431]: https://github.com/bevyengine/bevy/pull/9431 -[9436]: https://github.com/bevyengine/bevy/pull/9436 -[9444]: https://github.com/bevyengine/bevy/pull/9444 -[9446]: https://github.com/bevyengine/bevy/pull/9446 -[9448]: https://github.com/bevyengine/bevy/pull/9448 -[9451]: https://github.com/bevyengine/bevy/pull/9451 -[9453]: https://github.com/bevyengine/bevy/pull/9453 -[9456]: https://github.com/bevyengine/bevy/pull/9456 -[9460]: https://github.com/bevyengine/bevy/pull/9460 -[9461]: https://github.com/bevyengine/bevy/pull/9461 -[9462]: https://github.com/bevyengine/bevy/pull/9462 -[9463]: https://github.com/bevyengine/bevy/pull/9463 -[9464]: https://github.com/bevyengine/bevy/pull/9464 -[9467]: https://github.com/bevyengine/bevy/pull/9467 -[9468]: https://github.com/bevyengine/bevy/pull/9468 -[9469]: https://github.com/bevyengine/bevy/pull/9469 -[9470]: https://github.com/bevyengine/bevy/pull/9470 -[9479]: https://github.com/bevyengine/bevy/pull/9479 -[9480]: https://github.com/bevyengine/bevy/pull/9480 -[9481]: https://github.com/bevyengine/bevy/pull/9481 -[9482]: https://github.com/bevyengine/bevy/pull/9482 -[9484]: https://github.com/bevyengine/bevy/pull/9484 -[9485]: https://github.com/bevyengine/bevy/pull/9485 -[9486]: https://github.com/bevyengine/bevy/pull/9486 -[9489]: https://github.com/bevyengine/bevy/pull/9489 -[9490]: https://github.com/bevyengine/bevy/pull/9490 -[9493]: https://github.com/bevyengine/bevy/pull/9493 -[9497]: https://github.com/bevyengine/bevy/pull/9497 -[9505]: https://github.com/bevyengine/bevy/pull/9505 -[9506]: https://github.com/bevyengine/bevy/pull/9506 -[9513]: https://github.com/bevyengine/bevy/pull/9513 -[9514]: https://github.com/bevyengine/bevy/pull/9514 -[9516]: https://github.com/bevyengine/bevy/pull/9516 -[9518]: https://github.com/bevyengine/bevy/pull/9518 -[9528]: https://github.com/bevyengine/bevy/pull/9528 -[9529]: https://github.com/bevyengine/bevy/pull/9529 -[9539]: https://github.com/bevyengine/bevy/pull/9539 -[9542]: https://github.com/bevyengine/bevy/pull/9542 -[9545]: https://github.com/bevyengine/bevy/pull/9545 -[9547]: https://github.com/bevyengine/bevy/pull/9547 -[9548]: https://github.com/bevyengine/bevy/pull/9548 -[9551]: https://github.com/bevyengine/bevy/pull/9551 -[9553]: https://github.com/bevyengine/bevy/pull/9553 -[9554]: https://github.com/bevyengine/bevy/pull/9554 -[9555]: https://github.com/bevyengine/bevy/pull/9555 -[9562]: https://github.com/bevyengine/bevy/pull/9562 -[9565]: https://github.com/bevyengine/bevy/pull/9565 -[9566]: https://github.com/bevyengine/bevy/pull/9566 -[9568]: https://github.com/bevyengine/bevy/pull/9568 -[9576]: https://github.com/bevyengine/bevy/pull/9576 -[9577]: https://github.com/bevyengine/bevy/pull/9577 -[9579]: https://github.com/bevyengine/bevy/pull/9579 -[9581]: https://github.com/bevyengine/bevy/pull/9581 -[9583]: https://github.com/bevyengine/bevy/pull/9583 -[9587]: https://github.com/bevyengine/bevy/pull/9587 -[9588]: https://github.com/bevyengine/bevy/pull/9588 -[9589]: https://github.com/bevyengine/bevy/pull/9589 -[9592]: https://github.com/bevyengine/bevy/pull/9592 -[9596]: https://github.com/bevyengine/bevy/pull/9596 -[9597]: https://github.com/bevyengine/bevy/pull/9597 -[9598]: https://github.com/bevyengine/bevy/pull/9598 -[9600]: https://github.com/bevyengine/bevy/pull/9600 -[9604]: https://github.com/bevyengine/bevy/pull/9604 -[9609]: https://github.com/bevyengine/bevy/pull/9609 -[9610]: https://github.com/bevyengine/bevy/pull/9610 -[9611]: https://github.com/bevyengine/bevy/pull/9611 -[9612]: https://github.com/bevyengine/bevy/pull/9612 -[9613]: https://github.com/bevyengine/bevy/pull/9613 -[9614]: https://github.com/bevyengine/bevy/pull/9614 -[9617]: https://github.com/bevyengine/bevy/pull/9617 -[9619]: https://github.com/bevyengine/bevy/pull/9619 -[9621]: https://github.com/bevyengine/bevy/pull/9621 -[9626]: https://github.com/bevyengine/bevy/pull/9626 -[9629]: https://github.com/bevyengine/bevy/pull/9629 -[9630]: https://github.com/bevyengine/bevy/pull/9630 -[9631]: https://github.com/bevyengine/bevy/pull/9631 -[9637]: https://github.com/bevyengine/bevy/pull/9637 -[9638]: https://github.com/bevyengine/bevy/pull/9638 -[9640]: https://github.com/bevyengine/bevy/pull/9640 -[9646]: https://github.com/bevyengine/bevy/pull/9646 -[9648]: https://github.com/bevyengine/bevy/pull/9648 -[9650]: https://github.com/bevyengine/bevy/pull/9650 -[9653]: https://github.com/bevyengine/bevy/pull/9653 -[9657]: https://github.com/bevyengine/bevy/pull/9657 -[9659]: https://github.com/bevyengine/bevy/pull/9659 -[9662]: https://github.com/bevyengine/bevy/pull/9662 -[9665]: https://github.com/bevyengine/bevy/pull/9665 -[9666]: https://github.com/bevyengine/bevy/pull/9666 -[9669]: https://github.com/bevyengine/bevy/pull/9669 -[9672]: https://github.com/bevyengine/bevy/pull/9672 -[9673]: https://github.com/bevyengine/bevy/pull/9673 -[9674]: https://github.com/bevyengine/bevy/pull/9674 -[9675]: https://github.com/bevyengine/bevy/pull/9675 -[9677]: https://github.com/bevyengine/bevy/pull/9677 -[9679]: https://github.com/bevyengine/bevy/pull/9679 -[9684]: https://github.com/bevyengine/bevy/pull/9684 -[9685]: https://github.com/bevyengine/bevy/pull/9685 -[9686]: https://github.com/bevyengine/bevy/pull/9686 -[9692]: https://github.com/bevyengine/bevy/pull/9692 -[9694]: https://github.com/bevyengine/bevy/pull/9694 -[9697]: https://github.com/bevyengine/bevy/pull/9697 -[9699]: https://github.com/bevyengine/bevy/pull/9699 -[9704]: https://github.com/bevyengine/bevy/pull/9704 -[9705]: https://github.com/bevyengine/bevy/pull/9705 -[9712]: https://github.com/bevyengine/bevy/pull/9712 -[9716]: https://github.com/bevyengine/bevy/pull/9716 -[9720]: https://github.com/bevyengine/bevy/pull/9720 -[9726]: https://github.com/bevyengine/bevy/pull/9726 -[9729]: https://github.com/bevyengine/bevy/pull/9729 -[9733]: https://github.com/bevyengine/bevy/pull/9733 -[9734]: https://github.com/bevyengine/bevy/pull/9734 -[9736]: https://github.com/bevyengine/bevy/pull/9736 -[9737]: https://github.com/bevyengine/bevy/pull/9737 -[9739]: https://github.com/bevyengine/bevy/pull/9739 -[9741]: https://github.com/bevyengine/bevy/pull/9741 -[9742]: https://github.com/bevyengine/bevy/pull/9742 -[9743]: https://github.com/bevyengine/bevy/pull/9743 -[9744]: https://github.com/bevyengine/bevy/pull/9744 -[9748]: https://github.com/bevyengine/bevy/pull/9748 -[9749]: https://github.com/bevyengine/bevy/pull/9749 -[9751]: https://github.com/bevyengine/bevy/pull/9751 -[9752]: https://github.com/bevyengine/bevy/pull/9752 -[9753]: https://github.com/bevyengine/bevy/pull/9753 -[9756]: https://github.com/bevyengine/bevy/pull/9756 -[9757]: https://github.com/bevyengine/bevy/pull/9757 -[9759]: https://github.com/bevyengine/bevy/pull/9759 -[9767]: https://github.com/bevyengine/bevy/pull/9767 -[9769]: https://github.com/bevyengine/bevy/pull/9769 -[9778]: https://github.com/bevyengine/bevy/pull/9778 -[9779]: https://github.com/bevyengine/bevy/pull/9779 -[9781]: https://github.com/bevyengine/bevy/pull/9781 -[9784]: https://github.com/bevyengine/bevy/pull/9784 -[9785]: https://github.com/bevyengine/bevy/pull/9785 -[9794]: https://github.com/bevyengine/bevy/pull/9794 -[9795]: https://github.com/bevyengine/bevy/pull/9795 -[9796]: https://github.com/bevyengine/bevy/pull/9796 -[9800]: https://github.com/bevyengine/bevy/pull/9800 -[9801]: https://github.com/bevyengine/bevy/pull/9801 -[9803]: https://github.com/bevyengine/bevy/pull/9803 -[9804]: https://github.com/bevyengine/bevy/pull/9804 -[9807]: https://github.com/bevyengine/bevy/pull/9807 -[9812]: https://github.com/bevyengine/bevy/pull/9812 -[9814]: https://github.com/bevyengine/bevy/pull/9814 -[9818]: https://github.com/bevyengine/bevy/pull/9818 -[9820]: https://github.com/bevyengine/bevy/pull/9820 -[9823]: https://github.com/bevyengine/bevy/pull/9823 -[9824]: https://github.com/bevyengine/bevy/pull/9824 -[9825]: https://github.com/bevyengine/bevy/pull/9825 -[9826]: https://github.com/bevyengine/bevy/pull/9826 -[9829]: https://github.com/bevyengine/bevy/pull/9829 -[9830]: https://github.com/bevyengine/bevy/pull/9830 -[9833]: https://github.com/bevyengine/bevy/pull/9833 -[9834]: https://github.com/bevyengine/bevy/pull/9834 -[9835]: https://github.com/bevyengine/bevy/pull/9835 -[9836]: https://github.com/bevyengine/bevy/pull/9836 -[9839]: https://github.com/bevyengine/bevy/pull/9839 -[9840]: https://github.com/bevyengine/bevy/pull/9840 -[9842]: https://github.com/bevyengine/bevy/pull/9842 -[9844]: https://github.com/bevyengine/bevy/pull/9844 -[9845]: https://github.com/bevyengine/bevy/pull/9845 -[9846]: https://github.com/bevyengine/bevy/pull/9846 -[9847]: https://github.com/bevyengine/bevy/pull/9847 -[9849]: https://github.com/bevyengine/bevy/pull/9849 -[9850]: https://github.com/bevyengine/bevy/pull/9850 -[9851]: https://github.com/bevyengine/bevy/pull/9851 -[9853]: https://github.com/bevyengine/bevy/pull/9853 -[9861]: https://github.com/bevyengine/bevy/pull/9861 -[9865]: https://github.com/bevyengine/bevy/pull/9865 -[9867]: https://github.com/bevyengine/bevy/pull/9867 -[9870]: https://github.com/bevyengine/bevy/pull/9870 -[9871]: https://github.com/bevyengine/bevy/pull/9871 -[9872]: https://github.com/bevyengine/bevy/pull/9872 -[9873]: https://github.com/bevyengine/bevy/pull/9873 -[9878]: https://github.com/bevyengine/bevy/pull/9878 -[9879]: https://github.com/bevyengine/bevy/pull/9879 -[9881]: https://github.com/bevyengine/bevy/pull/9881 -[9882]: https://github.com/bevyengine/bevy/pull/9882 -[9885]: https://github.com/bevyengine/bevy/pull/9885 -[9895]: https://github.com/bevyengine/bevy/pull/9895 -[9899]: https://github.com/bevyengine/bevy/pull/9899 -[9900]: https://github.com/bevyengine/bevy/pull/9900 -[9903]: https://github.com/bevyengine/bevy/pull/9903 -[9909]: https://github.com/bevyengine/bevy/pull/9909 -[9912]: https://github.com/bevyengine/bevy/pull/9912 -[9913]: https://github.com/bevyengine/bevy/pull/9913 -[9919]: https://github.com/bevyengine/bevy/pull/9919 -[9922]: https://github.com/bevyengine/bevy/pull/9922 -[9923]: https://github.com/bevyengine/bevy/pull/9923 -[9925]: https://github.com/bevyengine/bevy/pull/9925 -[9927]: https://github.com/bevyengine/bevy/pull/9927 -[9931]: https://github.com/bevyengine/bevy/pull/9931 -[9933]: https://github.com/bevyengine/bevy/pull/9933 -[9934]: https://github.com/bevyengine/bevy/pull/9934 -[9935]: https://github.com/bevyengine/bevy/pull/9935 -[9936]: https://github.com/bevyengine/bevy/pull/9936 -[9937]: https://github.com/bevyengine/bevy/pull/9937 -[9939]: https://github.com/bevyengine/bevy/pull/9939 -[9941]: https://github.com/bevyengine/bevy/pull/9941 -[9945]: https://github.com/bevyengine/bevy/pull/9945 -[9949]: https://github.com/bevyengine/bevy/pull/9949 -[9950]: https://github.com/bevyengine/bevy/pull/9950 -[9951]: https://github.com/bevyengine/bevy/pull/9951 -[9956]: https://github.com/bevyengine/bevy/pull/9956 -[9958]: https://github.com/bevyengine/bevy/pull/9958 -[9959]: https://github.com/bevyengine/bevy/pull/9959 -[9961]: https://github.com/bevyengine/bevy/pull/9961 -[9963]: https://github.com/bevyengine/bevy/pull/9963 -[9965]: https://github.com/bevyengine/bevy/pull/9965 -[9966]: https://github.com/bevyengine/bevy/pull/9966 -[9974]: https://github.com/bevyengine/bevy/pull/9974 -[9981]: https://github.com/bevyengine/bevy/pull/9981 -[9982]: https://github.com/bevyengine/bevy/pull/9982 -[9984]: https://github.com/bevyengine/bevy/pull/9984 -[9989]: https://github.com/bevyengine/bevy/pull/9989 -[9990]: https://github.com/bevyengine/bevy/pull/9990 -[9991]: https://github.com/bevyengine/bevy/pull/9991 -[9992]: https://github.com/bevyengine/bevy/pull/9992 -[9993]: https://github.com/bevyengine/bevy/pull/9993 -[9994]: https://github.com/bevyengine/bevy/pull/9994 -[9998]: https://github.com/bevyengine/bevy/pull/9998 -[10001]: https://github.com/bevyengine/bevy/pull/10001 -[10002]: https://github.com/bevyengine/bevy/pull/10002 -[10003]: https://github.com/bevyengine/bevy/pull/10003 -[10008]: https://github.com/bevyengine/bevy/pull/10008 -[10010]: https://github.com/bevyengine/bevy/pull/10010 -[10012]: https://github.com/bevyengine/bevy/pull/10012 -[10020]: https://github.com/bevyengine/bevy/pull/10020 -[10022]: https://github.com/bevyengine/bevy/pull/10022 -[10023]: https://github.com/bevyengine/bevy/pull/10023 -[10026]: https://github.com/bevyengine/bevy/pull/10026 -[10030]: https://github.com/bevyengine/bevy/pull/10030 -[10031]: https://github.com/bevyengine/bevy/pull/10031 -[10035]: https://github.com/bevyengine/bevy/pull/10035 -[10044]: https://github.com/bevyengine/bevy/pull/10044 -[10051]: https://github.com/bevyengine/bevy/pull/10051 -[10053]: https://github.com/bevyengine/bevy/pull/10053 -[10056]: https://github.com/bevyengine/bevy/pull/10056 -[10058]: https://github.com/bevyengine/bevy/pull/10058 -[10065]: https://github.com/bevyengine/bevy/pull/10065 -[10068]: https://github.com/bevyengine/bevy/pull/10068 -[10070]: https://github.com/bevyengine/bevy/pull/10070 -[10074]: https://github.com/bevyengine/bevy/pull/10074 -[10078]: https://github.com/bevyengine/bevy/pull/10078 -[10079]: https://github.com/bevyengine/bevy/pull/10079 -[10080]: https://github.com/bevyengine/bevy/pull/10080 -[10090]: https://github.com/bevyengine/bevy/pull/10090 -[10092]: https://github.com/bevyengine/bevy/pull/10092 -[10100]: https://github.com/bevyengine/bevy/pull/10100 -[10103]: https://github.com/bevyengine/bevy/pull/10103 -[10105]: https://github.com/bevyengine/bevy/pull/10105 -[10109]: https://github.com/bevyengine/bevy/pull/10109 -[10111]: https://github.com/bevyengine/bevy/pull/10111 -[10117]: https://github.com/bevyengine/bevy/pull/10117 -[10119]: https://github.com/bevyengine/bevy/pull/10119 -[10121]: https://github.com/bevyengine/bevy/pull/10121 -[10123]: https://github.com/bevyengine/bevy/pull/10123 -[10124]: https://github.com/bevyengine/bevy/pull/10124 -[10128]: https://github.com/bevyengine/bevy/pull/10128 -[10132]: https://github.com/bevyengine/bevy/pull/10132 -[10136]: https://github.com/bevyengine/bevy/pull/10136 -[10142]: https://github.com/bevyengine/bevy/pull/10142 -[10143]: https://github.com/bevyengine/bevy/pull/10143 -[10144]: https://github.com/bevyengine/bevy/pull/10144 -[10145]: https://github.com/bevyengine/bevy/pull/10145 -[10148]: https://github.com/bevyengine/bevy/pull/10148 -[10150]: https://github.com/bevyengine/bevy/pull/10150 -[10151]: https://github.com/bevyengine/bevy/pull/10151 -[10152]: https://github.com/bevyengine/bevy/pull/10152 -[10156]: https://github.com/bevyengine/bevy/pull/10156 -[10158]: https://github.com/bevyengine/bevy/pull/10158 -[10160]: https://github.com/bevyengine/bevy/pull/10160 -[10161]: https://github.com/bevyengine/bevy/pull/10161 -[10163]: https://github.com/bevyengine/bevy/pull/10163 -[10168]: https://github.com/bevyengine/bevy/pull/10168 -[10171]: https://github.com/bevyengine/bevy/pull/10171 -[10173]: https://github.com/bevyengine/bevy/pull/10173 -[10174]: https://github.com/bevyengine/bevy/pull/10174 -[10178]: https://github.com/bevyengine/bevy/pull/10178 -[10179]: https://github.com/bevyengine/bevy/pull/10179 -[10180]: https://github.com/bevyengine/bevy/pull/10180 -[10182]: https://github.com/bevyengine/bevy/pull/10182 -[10184]: https://github.com/bevyengine/bevy/pull/10184 -[10185]: https://github.com/bevyengine/bevy/pull/10185 -[10186]: https://github.com/bevyengine/bevy/pull/10186 -[10189]: https://github.com/bevyengine/bevy/pull/10189 -[10191]: https://github.com/bevyengine/bevy/pull/10191 -[10194]: https://github.com/bevyengine/bevy/pull/10194 -[10195]: https://github.com/bevyengine/bevy/pull/10195 -[10196]: https://github.com/bevyengine/bevy/pull/10196 -[10197]: https://github.com/bevyengine/bevy/pull/10197 -[10198]: https://github.com/bevyengine/bevy/pull/10198 -[10200]: https://github.com/bevyengine/bevy/pull/10200 -[10202]: https://github.com/bevyengine/bevy/pull/10202 -[10204]: https://github.com/bevyengine/bevy/pull/10204 -[10206]: https://github.com/bevyengine/bevy/pull/10206 -[10208]: https://github.com/bevyengine/bevy/pull/10208 -[10210]: https://github.com/bevyengine/bevy/pull/10210 -[10211]: https://github.com/bevyengine/bevy/pull/10211 -[10217]: https://github.com/bevyengine/bevy/pull/10217 -[10221]: https://github.com/bevyengine/bevy/pull/10221 -[10222]: https://github.com/bevyengine/bevy/pull/10222 -[10223]: https://github.com/bevyengine/bevy/pull/10223 -[10226]: https://github.com/bevyengine/bevy/pull/10226 -[10227]: https://github.com/bevyengine/bevy/pull/10227 -[10230]: https://github.com/bevyengine/bevy/pull/10230 -[10238]: https://github.com/bevyengine/bevy/pull/10238 -[10240]: https://github.com/bevyengine/bevy/pull/10240 -[10249]: https://github.com/bevyengine/bevy/pull/10249 -[10251]: https://github.com/bevyengine/bevy/pull/10251 -[10252]: https://github.com/bevyengine/bevy/pull/10252 -[10253]: https://github.com/bevyengine/bevy/pull/10253 -[10254]: https://github.com/bevyengine/bevy/pull/10254 -[10259]: https://github.com/bevyengine/bevy/pull/10259 -[10262]: https://github.com/bevyengine/bevy/pull/10262 -[10264]: https://github.com/bevyengine/bevy/pull/10264 -[10270]: https://github.com/bevyengine/bevy/pull/10270 -[10271]: https://github.com/bevyengine/bevy/pull/10271 -[10275]: https://github.com/bevyengine/bevy/pull/10275 -[10276]: https://github.com/bevyengine/bevy/pull/10276 -[10279]: https://github.com/bevyengine/bevy/pull/10279 -[10286]: https://github.com/bevyengine/bevy/pull/10286 -[10289]: https://github.com/bevyengine/bevy/pull/10289 -[10294]: https://github.com/bevyengine/bevy/pull/10294 -[10295]: https://github.com/bevyengine/bevy/pull/10295 -[10297]: https://github.com/bevyengine/bevy/pull/10297 -[10299]: https://github.com/bevyengine/bevy/pull/10299 -[10303]: https://github.com/bevyengine/bevy/pull/10303 -[10304]: https://github.com/bevyengine/bevy/pull/10304 -[10306]: https://github.com/bevyengine/bevy/pull/10306 -[10310]: https://github.com/bevyengine/bevy/pull/10310 -[10311]: https://github.com/bevyengine/bevy/pull/10311 -[10312]: https://github.com/bevyengine/bevy/pull/10312 -[10326]: https://github.com/bevyengine/bevy/pull/10326 -[10330]: https://github.com/bevyengine/bevy/pull/10330 -[10339]: https://github.com/bevyengine/bevy/pull/10339 -[10341]: https://github.com/bevyengine/bevy/pull/10341 -[10345]: https://github.com/bevyengine/bevy/pull/10345 -[10346]: https://github.com/bevyengine/bevy/pull/10346 -[10352]: https://github.com/bevyengine/bevy/pull/10352 -[10356]: https://github.com/bevyengine/bevy/pull/10356 -[10358]: https://github.com/bevyengine/bevy/pull/10358 -[10360]: https://github.com/bevyengine/bevy/pull/10360 - -## Version 0.11.0 (2023-07-09) - -### Rendering - -- [Webgpu support][8336] -- [improve shader import model][5703] -- [Screen Space Ambient Occlusion (SSAO) MVP][7402] -- [Temporal Antialiasing (TAA)][7291] -- [Immediate Mode Line/Gizmo Drawing][6529] -- [Make render graph slots optional for most cases][8109] -- [Split opaque and transparent phases][8090] -- [Built-in skybox][8275] -- [Add parallax mapping to bevy PBR][5928] -- [Add port of AMD's Robust Contrast Adaptive Sharpening][7422] -- [Add RenderGraphApp to simplify adding render nodes][8007] -- [Add screenshot api][7163] -- [Add morph targets][8158] -- [Screenshots in wasm][8455] -- [Add ViewNode to simplify render node management][8118] -- [Bias texture mipmaps][7614] -- [Instanced line rendering for gizmos based on `bevy_polyline`][8427] -- [Add `RenderTarget::TextureView`][8042] -- [Change default tonemapping method][8685] -- [Allow custom depth texture usage][6815] -- [Use the prepass normal texture in main pass when possible][8231] -- [Left-handed y-up cubemap coordinates][8122] -- [Allow SPIR-V shaders to process when shader defs are present][7772] -- [Remove unnecesssary values Vec from DynamicUniformBuffer and DynamicStorageBuffer][8299] -- [Add `MAY_DISCARD` shader def, enabling early depth tests for most cases][6697] -- [Add `Aabb` calculation for `Sprite`, `TextureAtlasSprite` and `Mesh2d`][7885] -- [Color::Lcha constructors][8041] -- [Fix Color::as_rgba_linear for Color::Lcha][8040] -- [Added Globals struct to prepass shader][8070] -- [Derive Copy and Clone for Collision][8121] -- [Fix crash when enabling HDR on 2d cameras][8151] -- [Dither fix][7977] -- [Compute `vertex_count` for indexed meshes on `GpuMesh`][8460] -- [Run update_previous_view_projections in PreUpdate schedule][9024] -- [Added `WebP` image format support][8220] -- [Add support for pnm textures][8601] -- [fix invalid bone weights][8316] -- [Fix pbr shader breaking on missing UVs][8412] -- [Fix Plane UVs / texture flip][8878] -- [Fix look_to resulting in NaN rotations][7817] -- [Fix look_to variable naming][8627] -- [Fix segfault with 2d gizmos][8223] -- [Use RenderGraphApp in more places][8298] -- [Fix viewport change detection][8323] -- [Remove capacity fields from all Buffer wrapper types][8301] -- [Sync pbr_types.wgsl StandardMaterial values][8380] -- [Avoid spawning gizmo meshes when no gizmos are being drawn][8180] -- [Use a consistent seed for AABB gizmo colors][9030] -- [bevy_pbr: Do not cull meshes without Aabbs from cascades][8444] -- [Handle vertex_uvs if they are present in default prepass fragment shader][8330] -- [Changed (Vec2, Vec2) to Rect in Camera::logical_viewport_rect][7867] -- [make glsl and spirv support optional][8491] -- [fix prepass normal_mapping][8978] -- [conversions between [u8; 4] and Color][8564] -- [Add option to disable gizmo rendering for specific cameras][8952] -- [Fix morph target prepass shader][9013] -- [Fix bloom wasm support][8631] -- [Fix black spots appearing due to NANs when SSAO is enabled][8926] -- [fix normal prepass][8890] -- [Refs #8975 -- Add return to RenderDevice::poll()][8977] -- [Fix WebGL mode for Adreno GPUs][8508] -- [Fix parallax mapping][9003] -- [Added Vec append to BufferVec - Issue #3531][8575] -- [Fix CAS shader with explicit FullscreenVertexOutput import][8993] -- [Make `TextureAtlas::texture_handles` `pub` instead of `pub(crate)` (#8633)][8643] -- [Make Material2d pipeline systems public][8642] -- [Fix screenshots on Wayland + Nvidia][8701] -- [Apply codebase changes in preparation for `StandardMaterial` transmission][8704] -- [Use ViewNode for TAA][8732] -- [Change Camera3dBundle::tonemapping to Default][8753] -- [Remove `Component` derive for AlphaMode][8804] -- [Make setup of Opaque3dPrepass and AlphaMask3dPrepass phase items consistent with others][8408] -- [Rename `Plane` struct to `HalfSpace`][8744] -- [Expand `FallbackImage` to include a `GpuImage` for each possible `TextureViewDimension`][6974] -- [Cascaded shadow maps: Fix prepass ortho depth clamping][8877] -- [Fix gizmos in WebGPU][8910] -- [Fix AsBindGroup derive, texture attribute, visibility flag parsing][8868] -- [Disable camera on window close][8802] -- [Reflect `Component` and `Default` of `BloomSettings`][8283] -- [Add Reflection Macros to TextureAtlasSprite][8428] -- [Implement Reflect on NoFrustumCulling][8801] - -### Audio - -- [ECS-based API redesign][8424] -- [Ability to set a Global Volume][7706] -- [Expose `AudioSink::empty()`][8145] - -### Diagnostics - -- [Allow systems using Diagnostics to run in parallel][8677] -- [add a feature for memory tracing with tracy][8272] -- [Re-add the "frame" span for tracy comparisons][8362] -- [log to stderr instead of stdout][8886] - -### Scenes - -- [bevy_scene: Add SceneFilter][6793] -- [(De) serialize resources in scenes][6846] -- [add position to scene errors][8065] -- [Bugfix: Scene reload fix (nonbreaking)][7951] -- [avoid panic with parented scenes on deleted entities][8512] - -### Transform + Hierarchy - -- [Fix transform propagation of orphaned entities][7264] - -### Gizmo - -- [Add a bounding box gizmo][8468] -- [Added `arc_2d` function for gizmos][8448] -- [Use AHash to get color from entity in bevy_gizmos][8960] -- [do not crash when rendering only one gizmo][8434] - -### Reflection - -- [reflect: stable type path v2][7184] -- [bevy_reflect: Better proxies][6971] -- [bevy_reflect: FromReflect Ergonomics Implementation][6056] -- [bevy_reflect: Allow `#[reflect(default)]` on enum variant fields][8514] -- [Add FromReflect where Reflect is used][8776] -- [Add get_at_mut to bevy_reflect::Map trait][8691] -- [Reflect now requires DynamicTypePath. Remove Reflect::get_type_path()][8764] -- [bevy_ui: Add `FromReflect` derives][8495] -- [Add Reflect and FromReflect for AssetPath][8531] -- [bevy_reflect: Fix trailing comma breaking derives][8014] -- [Fix Box dyn Reflect struct with a hashmap in it panicking when clone_value is called on it][8184] -- [bevy_reflect: Add `ReflectFromReflect` to the prelude][8496] -- [bevy_reflect: Allow construction of MapIter outside of the bevy_reflect crate.][8723] -- [bevy_reflect: Disambiguate type bounds in where clauses.][8761] -- [adding reflection for Cow<'static, [T]>][7454] -- [Do not require mut on ParsedPath::element_mut][8891] -- [Reflect UUID][8905] -- [Don't ignore additional entries in `UntypedReflectDeserializerVisitor`][7112] -- [Construct Box dyn Reflect from world for ReflectComponent][7407] -- [reflect: avoid deadlock in GenericTypeCell][8957] - -### App - -- [Allow tuples and single plugins in `add_plugins`, deprecate `add_plugin`][8097] -- [Merge ScheduleRunnerSettings into ScheduleRunnerPlugin][8585] -- [correctly setup everything in the default run_once runner][8740] -- [Fix `Plugin::build` detection][8103] -- [Fix not calling App::finish and App::cleanup in `ScheduleRunnerPlugin`][9054] -- [Relaxed runner type from Fn to FnOnce][8961] -- [Relax FnMut to FnOnce in app::edit_schedule][8982] - -### Windowing + Reflection - -- [Register missing types in bevy_window][7993] -- [bevy_reflect: implement Reflect for SmolStr][8771] - -### Hierarchy - -- [fix panic when moving child][8346] -- [Remove `Children` component when calling `despawn_descendants`][8476] -- [Change `despawn_descendants` to return `&mut Self`][8928] - -### Time - -- [Fix timer with zero duration][8467] - -### Assets - -- [Delay asset hot reloading][8503] -- [Add support for custom glTF vertex attributes.][5370] -- [Fix panic when using debug_asset_server][8485] -- [`unused_variables` warning when building with `filesystem_watcher` feature disabled][7938] -- [bevy_asset: Add `LoadContext::get_handle_untyped`][8470] - -### Windowing - -- [Move cursor position to internal state][7988] -- [Set cursor hittest during window creation][7966] -- [do not set hit test unconditionally on window creation][7996] -- [Add winit's `wayland-csd-adwaita` feature to Bevy's `wayland` feature][8722] -- [Support to set window theme and expose system window theme changed event][8593] -- [Touchpad magnify and rotate events][8791] -- [Fix windows not being centered properly when system interface is scaled][8903] -- [Expose WindowDestroyed events][9016] - -### Animation - -- [Register bevy_animation::PlayingAnimation][9023] - -### UI - -- [Ui Node Borders][7795] -- [Add CSS Grid support to `bevy_ui`][8026] -- [`text_system` split][7779] -- [Replace the local text queues in the text systems with flags stored in a component][8549] -- [`NoWrap` `Text` feature][8947] -- [add a default font][8445] -- [UI texture atlas support][8822] -- [Improved UI render batching][8793] -- [Consistent screen-space coordinates][8306] -- [`UiImage` helper functions][8199] -- [Perform text scaling calculations per text, not per glyph][7819] -- [Fix size of clipped text glyphs.][8197] -- [Apply scale factor to `ImageMeasure` sizes][8545] -- [Fix WebGPU error in "ui_pipeline" by adding a flat interpolate attribute][8933] -- [Rename Interaction::Clicked -> Interaction::Pressed][9027] -- [Flatten UI `Style` properties that use `Size` + remove `Size`][8548] -- [Split UI `Overflow` by axis][8095] -- [Add methods for calculating the size and postion of UI nodes][7930] -- [Skip the UV calculations for untextured UI nodes][7809] -- [Fix text measurement algorithm][8425] -- [Divide by UiScale when converting UI coordinates from physical to logical][8720] -- [`MeasureFunc` improvements][8402] -- [Expose sorting methods in `Children`][8522] -- [Fix min and max size using size value][7948] -- [Fix the `Text2d` text anchor's incorrect horizontal alignment][8019] -- [Remove `Val::Undefined`][7485] -- [`Val` viewport unit variants][8137] -- [Remove the corresponding measure from Taffy when a `CalculatedSize` component is removed.][8294] -- [`UiRect` axes constructor][7656] -- [Fix the UV calculations for clipped and flipped ImageNodes][8195] -- [Fix text systems broken when resolving merge conflicts in #8026][8422] -- [Allow `bevy_ui` crate to compile without the `text` feature enabled][8437] -- [Fix the double leaf node updates in `flex_node_system`][8264] -- [also import the default handle when feature disabled][8456] -- [`measure_text_system` text query fix][8466] -- [Fix panic in example: text_wrap_debug.rs][8497] -- [UI layout tree debug print][8521] -- [Fix `Node::physical_rect` and add a `physical_size` method][8551] -- [Perform `relative_cursor_position` calculation vectorwise in `ui_focus_system`][8795] -- [Add `UiRect::px()` and `UiRect::percent()` utils][8866] -- [Add missing dependencies to `bevy_text` feature][8920] -- [Remove "bevy_text" feature attributes on imports used by non-text systems][8907] -- [Growing UI nodes Fix][8931] - -### ECS - -- [Schedule-First: the new and improved add_systems][8079] -- [Add OnTransition schedule that is ran between OnExit and OnEnter][7936] -- [`run_if` for `SystemConfigs` via anonymous system sets][7676] -- [Remove OnUpdate system set][8260] -- [Rename apply_system_buffers to apply_deferred][8726] -- [Rename Command's "write" method to "apply"][8814] -- [Require `#[derive(Event)]` on all Events][7086] -- [Implement WorldQuery for EntityRef][6960] -- [Improve or-with disjoint checks][7085] -- [Add a method to run read-only systems using `&World`][8849] -- [Reduce branching when inserting components][8053] -- [Make `#[system_param(ignore)]` and `#[world_query(ignore)]` unnecessary][8030] -- [Remove `#[system_param(ignore)]` and `#[world_query(ignore)]`][8265] -- [Extend the `WorldQuery` macro to tuple structs][8119] -- [Make state private and only accessible through getter for State resource][8009] -- [implement `Deref` for `State`][8668] -- [Inline more ECS functions][8083] -- [Add a `scope` API for world schedules][8387] -- [Simplify system piping and make it more flexible][8377] -- [Add `any_component_removed` condition][8326] -- [Use `UnsafeWorldCell` to increase code quality for `SystemParam`][8174] -- [Improve safety for the multi-threaded executor using `UnsafeWorldCell`][8292] -- [Migrate the rest of the engine to `UnsafeWorldCell`][8833] -- [Make the `Condition` trait generic][8721] -- [Add or_else combinator to run_conditions.rs][8714] -- [Add iter_many_manual QueryState method][8772] -- [Provide access to world storages via UnsafeWorldCell][8987] -- [Added Has T WorldQuery type][8844] -- [Add/fix `track_caller` attribute on panicking entity accessor methods][8951] -- [Increase type safety and clarity for change detection][7905] -- [Make `WorldQuery` meta types unnameable][7964] -- [Add a public constructor for `Mut`][7931] -- [Remove ChangeTrackers][7902] -- [Derive Eq, PartialEq for Tick][9020] -- [Initialize empty schedules when calling `.in_schedule` if they do not already exist][7911] -- [Replace multiple calls to `add_system` with `add_systems`][8001] -- [don't panic on unknown ambiguity][7950] -- [add Clone to common conditions][8060] -- [Make BundleInfo's fields not pub(crate)][8068] -- [Pass query change ticks to `QueryParIter` instead of always using change ticks from `World`.][8029] -- [Remove redundant bounds check in `Entities::get`][8108] -- [Add World::try_run_schedule][8028] -- [change not implemation to custom system struct][8105] -- [Fix name conflicts caused by the `SystemParam` and `WorldQuery` macros][8012] -- [Check for conflicting accesses in `assert_is_system`][8154] -- [Fix field visibility for read-only `WorldQuery` types][8163] -- [`Or` should be a new type of `PhantomData`][8212] -- [Make standard commands more ergonomic (in niche cases)][8249] -- [Remove base set error variants of `ScheduleBuildError`][8269] -- [Replace some unsafe system executor code with safe code][8274] -- [Update `increment_change_tick` to return a strongly-typed `Tick`][8295] -- [Move event traces to detailed_trace!][7732] -- [Only trigger state transitons if `next_state != old_state`][8359] -- [Fix panics and docs when using World schedules][8364] -- [Improve warning for Send resources marked as non_send][8000] -- [Reorganize system modules][8419] -- [Fix boxed labels][8436] -- [Simplify world schedule methods][8403] -- [Just print out name string, not the entire Name struct][8494] -- [Manually implement common traits for `EventId`][8529] -- [Replace remaining uses of `&T, Changed` with `Ref` in UI system queries][8567] -- [Rename `UnsafeWorldCell::read_change_tick`][8588] -- [Improve encapsulation for commands and add docs][8725] -- [Fix all_tuples + added docs.][8743] -- [Add `new` and `map` methods to `Ref`][8797] -- [Allow unsized types as mapped value in `Ref::map`][8817] -- [Implement `Clone` for `CombinatorSystem`][8826] -- [Add get_ref to EntityRef][8818] -- [Make `QueryParIter::for_each_unchecked` private][8848] -- [Simplify the `ComponentIdFor` type][8845] -- [Add last_changed_tick and added_tick to ComponentTicks][8803] -- [Require read-only queries in `QueryState::par_iter`][8832] -- [Fix any_component_removed][8939] -- [Deprecate type aliases for `WorldQuery::Fetch`][8843] -- [bevy_ecs: add untyped methods for inserting components and bundles][7204] -- [Move AppTypeRegistry to bevy_ecs][8901] -- [skip check change tick for apply_deferred systems][8760] -- [Split the bevy_ecs reflect.rs module][8834] -- [Make function pointers of ecs Reflect* public][8687] - -### Rendering + Reflection + Scenes - -- [fix: register Cascade in the TypeRegistry][8088] - -### Tasks - -- [Add optional single-threaded feature to bevy_ecs/bevy_tasks][6690] - -### Math - -- [Re-export glam_assert feature][8232] -- [Fix CubicCurve::iter_samples iteration count][8049] -- [Add integer equivalents for `Rect`][7984] -- [Add `CubicCurve::segment_count` + `iter_samples` adjustment][8711] - -### Rendering + Assets + Meta - -- [Add depending bevy features for higher level one][7855] - -### ECS + Scenes - -- [Make scene handling of entity references robust][7335] -- [Rename map_entities and map_specific_entities][7570] - -### Util - -- [bevy_derive: Add `#[deref]` attribute][8552] - -### Input - -- [Add gamepad rumble support to bevy_input][8398] -- [Rename keys like `LAlt` to `AltLeft`][8792] -- [Add window entity to mouse and keyboard events][8852] -- [Add get_unclamped to Axis][8871] - -### Upgrades - -- [Upgrade Taffy requirement to v0.3.5][7959] -- [Update ruzstd and basis universal][8622] -- [Updated to wgpu 0.16.0, wgpu-hal 0.16.0 and naga 0.12.0][8446] -- [Update sysinfo requirement from 0.28.1 to 0.29.0][8650] -- [Update libloading requirement from 0.7 to 0.8][8649] -- [update syn, encase, glam and hexasphere][8573] -- [Update android_log-sys requirement from 0.2.0 to 0.3.0][7925] -- [update bitflags to 2.3][8728] -- [Update ruzstd requirement from 0.3.1 to 0.4.0][8755] -- [Update notify requirement from 5.0.0 to 6.0.0][8757] -- [Bump hashbrown to 0.14][8904] -- [update ahash and hashbrown][8623] -- [Bump accesskit and accesskit_winit][8655] - -### Examples - -- [new example showcase tool][8561] -- [Adding a bezier curve example][8194] -- [Add low level post process example using a custom render pass][6909] -- [Add example to demonstrate manual generation and UV mapping of 3D mesh (generate_custom_mesh) solve #4922][8909] -- [Add `overflow_debug` example][8198] -- [UI text wrapping and `LineBreakOn` example][7761] -- [Size Constraints Example][7956] -- [UI Display and Visibility Example][7629] - -[5370]: https://github.com/bevyengine/bevy/pull/5370 -[5703]: https://github.com/bevyengine/bevy/pull/5703 -[5928]: https://github.com/bevyengine/bevy/pull/5928 -[6529]: https://github.com/bevyengine/bevy/pull/6529 -[6697]: https://github.com/bevyengine/bevy/pull/6697 -[6815]: https://github.com/bevyengine/bevy/pull/6815 -[6846]: https://github.com/bevyengine/bevy/pull/6846 -[6909]: https://github.com/bevyengine/bevy/pull/6909 -[6960]: https://github.com/bevyengine/bevy/pull/6960 -[6971]: https://github.com/bevyengine/bevy/pull/6971 -[6974]: https://github.com/bevyengine/bevy/pull/6974 -[7085]: https://github.com/bevyengine/bevy/pull/7085 -[7086]: https://github.com/bevyengine/bevy/pull/7086 -[7112]: https://github.com/bevyengine/bevy/pull/7112 -[7163]: https://github.com/bevyengine/bevy/pull/7163 -[7184]: https://github.com/bevyengine/bevy/pull/7184 -[7204]: https://github.com/bevyengine/bevy/pull/7204 -[7264]: https://github.com/bevyengine/bevy/pull/7264 -[7291]: https://github.com/bevyengine/bevy/pull/7291 -[7335]: https://github.com/bevyengine/bevy/pull/7335 -[7402]: https://github.com/bevyengine/bevy/pull/7402 -[7407]: https://github.com/bevyengine/bevy/pull/7407 -[7422]: https://github.com/bevyengine/bevy/pull/7422 -[7454]: https://github.com/bevyengine/bevy/pull/7454 -[7485]: https://github.com/bevyengine/bevy/pull/7485 -[7570]: https://github.com/bevyengine/bevy/pull/7570 -[7614]: https://github.com/bevyengine/bevy/pull/7614 -[7629]: https://github.com/bevyengine/bevy/pull/7629 -[7656]: https://github.com/bevyengine/bevy/pull/7656 -[7676]: https://github.com/bevyengine/bevy/pull/7676 -[7706]: https://github.com/bevyengine/bevy/pull/7706 -[7732]: https://github.com/bevyengine/bevy/pull/7732 -[7761]: https://github.com/bevyengine/bevy/pull/7761 -[7772]: https://github.com/bevyengine/bevy/pull/7772 -[7779]: https://github.com/bevyengine/bevy/pull/7779 -[7795]: https://github.com/bevyengine/bevy/pull/7795 -[7809]: https://github.com/bevyengine/bevy/pull/7809 -[7817]: https://github.com/bevyengine/bevy/pull/7817 -[7819]: https://github.com/bevyengine/bevy/pull/7819 -[7855]: https://github.com/bevyengine/bevy/pull/7855 -[7867]: https://github.com/bevyengine/bevy/pull/7867 -[7885]: https://github.com/bevyengine/bevy/pull/7885 -[7902]: https://github.com/bevyengine/bevy/pull/7902 -[7905]: https://github.com/bevyengine/bevy/pull/7905 -[7911]: https://github.com/bevyengine/bevy/pull/7911 -[7925]: https://github.com/bevyengine/bevy/pull/7925 -[7930]: https://github.com/bevyengine/bevy/pull/7930 -[7931]: https://github.com/bevyengine/bevy/pull/7931 -[7936]: https://github.com/bevyengine/bevy/pull/7936 -[7938]: https://github.com/bevyengine/bevy/pull/7938 -[7948]: https://github.com/bevyengine/bevy/pull/7948 -[7950]: https://github.com/bevyengine/bevy/pull/7950 -[7951]: https://github.com/bevyengine/bevy/pull/7951 -[7956]: https://github.com/bevyengine/bevy/pull/7956 -[7959]: https://github.com/bevyengine/bevy/pull/7959 -[7964]: https://github.com/bevyengine/bevy/pull/7964 -[7966]: https://github.com/bevyengine/bevy/pull/7966 -[7977]: https://github.com/bevyengine/bevy/pull/7977 -[7984]: https://github.com/bevyengine/bevy/pull/7984 -[7988]: https://github.com/bevyengine/bevy/pull/7988 -[7993]: https://github.com/bevyengine/bevy/pull/7993 -[7996]: https://github.com/bevyengine/bevy/pull/7996 -[8000]: https://github.com/bevyengine/bevy/pull/8000 -[8001]: https://github.com/bevyengine/bevy/pull/8001 -[8007]: https://github.com/bevyengine/bevy/pull/8007 -[8009]: https://github.com/bevyengine/bevy/pull/8009 -[8012]: https://github.com/bevyengine/bevy/pull/8012 -[8014]: https://github.com/bevyengine/bevy/pull/8014 -[8019]: https://github.com/bevyengine/bevy/pull/8019 -[8026]: https://github.com/bevyengine/bevy/pull/8026 -[8028]: https://github.com/bevyengine/bevy/pull/8028 -[8029]: https://github.com/bevyengine/bevy/pull/8029 -[8030]: https://github.com/bevyengine/bevy/pull/8030 -[8040]: https://github.com/bevyengine/bevy/pull/8040 -[8041]: https://github.com/bevyengine/bevy/pull/8041 -[8042]: https://github.com/bevyengine/bevy/pull/8042 -[8049]: https://github.com/bevyengine/bevy/pull/8049 -[8053]: https://github.com/bevyengine/bevy/pull/8053 -[8060]: https://github.com/bevyengine/bevy/pull/8060 -[8065]: https://github.com/bevyengine/bevy/pull/8065 -[8068]: https://github.com/bevyengine/bevy/pull/8068 -[8070]: https://github.com/bevyengine/bevy/pull/8070 -[8079]: https://github.com/bevyengine/bevy/pull/8079 -[8083]: https://github.com/bevyengine/bevy/pull/8083 -[8088]: https://github.com/bevyengine/bevy/pull/8088 -[8090]: https://github.com/bevyengine/bevy/pull/8090 -[8095]: https://github.com/bevyengine/bevy/pull/8095 -[8097]: https://github.com/bevyengine/bevy/pull/8097 -[8103]: https://github.com/bevyengine/bevy/pull/8103 -[8105]: https://github.com/bevyengine/bevy/pull/8105 -[8108]: https://github.com/bevyengine/bevy/pull/8108 -[8109]: https://github.com/bevyengine/bevy/pull/8109 -[8118]: https://github.com/bevyengine/bevy/pull/8118 -[8119]: https://github.com/bevyengine/bevy/pull/8119 -[8121]: https://github.com/bevyengine/bevy/pull/8121 -[8122]: https://github.com/bevyengine/bevy/pull/8122 -[8137]: https://github.com/bevyengine/bevy/pull/8137 -[8145]: https://github.com/bevyengine/bevy/pull/8145 -[8151]: https://github.com/bevyengine/bevy/pull/8151 -[8154]: https://github.com/bevyengine/bevy/pull/8154 -[8158]: https://github.com/bevyengine/bevy/pull/8158 -[8163]: https://github.com/bevyengine/bevy/pull/8163 -[8174]: https://github.com/bevyengine/bevy/pull/8174 -[8180]: https://github.com/bevyengine/bevy/pull/8180 -[8184]: https://github.com/bevyengine/bevy/pull/8184 -[8194]: https://github.com/bevyengine/bevy/pull/8194 -[8195]: https://github.com/bevyengine/bevy/pull/8195 -[8197]: https://github.com/bevyengine/bevy/pull/8197 -[8198]: https://github.com/bevyengine/bevy/pull/8198 -[8199]: https://github.com/bevyengine/bevy/pull/8199 -[8212]: https://github.com/bevyengine/bevy/pull/8212 -[8220]: https://github.com/bevyengine/bevy/pull/8220 -[8223]: https://github.com/bevyengine/bevy/pull/8223 -[8231]: https://github.com/bevyengine/bevy/pull/8231 -[8232]: https://github.com/bevyengine/bevy/pull/8232 -[8249]: https://github.com/bevyengine/bevy/pull/8249 -[8260]: https://github.com/bevyengine/bevy/pull/8260 -[8264]: https://github.com/bevyengine/bevy/pull/8264 -[8265]: https://github.com/bevyengine/bevy/pull/8265 -[8269]: https://github.com/bevyengine/bevy/pull/8269 -[8272]: https://github.com/bevyengine/bevy/pull/8272 -[8274]: https://github.com/bevyengine/bevy/pull/8274 -[8275]: https://github.com/bevyengine/bevy/pull/8275 -[8283]: https://github.com/bevyengine/bevy/pull/8283 -[8292]: https://github.com/bevyengine/bevy/pull/8292 -[8294]: https://github.com/bevyengine/bevy/pull/8294 -[8295]: https://github.com/bevyengine/bevy/pull/8295 -[8298]: https://github.com/bevyengine/bevy/pull/8298 -[8299]: https://github.com/bevyengine/bevy/pull/8299 -[8301]: https://github.com/bevyengine/bevy/pull/8301 -[8306]: https://github.com/bevyengine/bevy/pull/8306 -[8316]: https://github.com/bevyengine/bevy/pull/8316 -[8323]: https://github.com/bevyengine/bevy/pull/8323 -[8326]: https://github.com/bevyengine/bevy/pull/8326 -[8330]: https://github.com/bevyengine/bevy/pull/8330 -[8336]: https://github.com/bevyengine/bevy/pull/8336 -[8346]: https://github.com/bevyengine/bevy/pull/8346 -[8359]: https://github.com/bevyengine/bevy/pull/8359 -[8362]: https://github.com/bevyengine/bevy/pull/8362 -[8364]: https://github.com/bevyengine/bevy/pull/8364 -[8377]: https://github.com/bevyengine/bevy/pull/8377 -[8380]: https://github.com/bevyengine/bevy/pull/8380 -[8387]: https://github.com/bevyengine/bevy/pull/8387 -[8398]: https://github.com/bevyengine/bevy/pull/8398 -[8402]: https://github.com/bevyengine/bevy/pull/8402 -[8403]: https://github.com/bevyengine/bevy/pull/8403 -[8408]: https://github.com/bevyengine/bevy/pull/8408 -[8412]: https://github.com/bevyengine/bevy/pull/8412 -[8419]: https://github.com/bevyengine/bevy/pull/8419 -[8422]: https://github.com/bevyengine/bevy/pull/8422 -[8425]: https://github.com/bevyengine/bevy/pull/8425 -[8427]: https://github.com/bevyengine/bevy/pull/8427 -[8428]: https://github.com/bevyengine/bevy/pull/8428 -[8434]: https://github.com/bevyengine/bevy/pull/8434 -[8436]: https://github.com/bevyengine/bevy/pull/8436 -[8437]: https://github.com/bevyengine/bevy/pull/8437 -[8444]: https://github.com/bevyengine/bevy/pull/8444 -[8445]: https://github.com/bevyengine/bevy/pull/8445 -[8446]: https://github.com/bevyengine/bevy/pull/8446 -[8448]: https://github.com/bevyengine/bevy/pull/8448 -[8455]: https://github.com/bevyengine/bevy/pull/8455 -[8456]: https://github.com/bevyengine/bevy/pull/8456 -[8460]: https://github.com/bevyengine/bevy/pull/8460 -[8466]: https://github.com/bevyengine/bevy/pull/8466 -[8467]: https://github.com/bevyengine/bevy/pull/8467 -[8468]: https://github.com/bevyengine/bevy/pull/8468 -[8470]: https://github.com/bevyengine/bevy/pull/8470 -[8476]: https://github.com/bevyengine/bevy/pull/8476 -[8485]: https://github.com/bevyengine/bevy/pull/8485 -[8491]: https://github.com/bevyengine/bevy/pull/8491 -[8494]: https://github.com/bevyengine/bevy/pull/8494 -[8495]: https://github.com/bevyengine/bevy/pull/8495 -[8496]: https://github.com/bevyengine/bevy/pull/8496 -[8497]: https://github.com/bevyengine/bevy/pull/8497 -[8503]: https://github.com/bevyengine/bevy/pull/8503 -[8512]: https://github.com/bevyengine/bevy/pull/8512 -[8514]: https://github.com/bevyengine/bevy/pull/8514 -[8521]: https://github.com/bevyengine/bevy/pull/8521 -[8522]: https://github.com/bevyengine/bevy/pull/8522 -[8529]: https://github.com/bevyengine/bevy/pull/8529 -[8531]: https://github.com/bevyengine/bevy/pull/8531 -[8545]: https://github.com/bevyengine/bevy/pull/8545 -[8548]: https://github.com/bevyengine/bevy/pull/8548 -[8549]: https://github.com/bevyengine/bevy/pull/8549 -[8551]: https://github.com/bevyengine/bevy/pull/8551 -[8552]: https://github.com/bevyengine/bevy/pull/8552 -[8561]: https://github.com/bevyengine/bevy/pull/8561 -[8564]: https://github.com/bevyengine/bevy/pull/8564 -[8567]: https://github.com/bevyengine/bevy/pull/8567 -[8573]: https://github.com/bevyengine/bevy/pull/8573 -[8575]: https://github.com/bevyengine/bevy/pull/8575 -[8585]: https://github.com/bevyengine/bevy/pull/8585 -[8588]: https://github.com/bevyengine/bevy/pull/8588 -[8593]: https://github.com/bevyengine/bevy/pull/8593 -[8601]: https://github.com/bevyengine/bevy/pull/8601 -[8622]: https://github.com/bevyengine/bevy/pull/8622 -[8623]: https://github.com/bevyengine/bevy/pull/8623 -[8627]: https://github.com/bevyengine/bevy/pull/8627 -[8631]: https://github.com/bevyengine/bevy/pull/8631 -[8642]: https://github.com/bevyengine/bevy/pull/8642 -[8643]: https://github.com/bevyengine/bevy/pull/8643 -[8649]: https://github.com/bevyengine/bevy/pull/8649 -[8650]: https://github.com/bevyengine/bevy/pull/8650 -[8668]: https://github.com/bevyengine/bevy/pull/8668 -[8677]: https://github.com/bevyengine/bevy/pull/8677 -[8685]: https://github.com/bevyengine/bevy/pull/8685 -[8687]: https://github.com/bevyengine/bevy/pull/8687 -[8691]: https://github.com/bevyengine/bevy/pull/8691 -[8701]: https://github.com/bevyengine/bevy/pull/8701 -[8704]: https://github.com/bevyengine/bevy/pull/8704 -[8711]: https://github.com/bevyengine/bevy/pull/8711 -[8714]: https://github.com/bevyengine/bevy/pull/8714 -[8721]: https://github.com/bevyengine/bevy/pull/8721 -[8722]: https://github.com/bevyengine/bevy/pull/8722 -[8723]: https://github.com/bevyengine/bevy/pull/8723 -[8725]: https://github.com/bevyengine/bevy/pull/8725 -[8726]: https://github.com/bevyengine/bevy/pull/8726 -[8728]: https://github.com/bevyengine/bevy/pull/8728 -[8732]: https://github.com/bevyengine/bevy/pull/8732 -[8740]: https://github.com/bevyengine/bevy/pull/8740 -[8743]: https://github.com/bevyengine/bevy/pull/8743 -[8744]: https://github.com/bevyengine/bevy/pull/8744 -[8753]: https://github.com/bevyengine/bevy/pull/8753 -[8755]: https://github.com/bevyengine/bevy/pull/8755 -[8757]: https://github.com/bevyengine/bevy/pull/8757 -[8760]: https://github.com/bevyengine/bevy/pull/8760 -[8761]: https://github.com/bevyengine/bevy/pull/8761 -[8764]: https://github.com/bevyengine/bevy/pull/8764 -[8771]: https://github.com/bevyengine/bevy/pull/8771 -[8772]: https://github.com/bevyengine/bevy/pull/8772 -[8776]: https://github.com/bevyengine/bevy/pull/8776 -[8791]: https://github.com/bevyengine/bevy/pull/8791 -[8792]: https://github.com/bevyengine/bevy/pull/8792 -[8793]: https://github.com/bevyengine/bevy/pull/8793 -[8795]: https://github.com/bevyengine/bevy/pull/8795 -[8797]: https://github.com/bevyengine/bevy/pull/8797 -[8801]: https://github.com/bevyengine/bevy/pull/8801 -[8802]: https://github.com/bevyengine/bevy/pull/8802 -[8803]: https://github.com/bevyengine/bevy/pull/8803 -[8804]: https://github.com/bevyengine/bevy/pull/8804 -[8814]: https://github.com/bevyengine/bevy/pull/8814 -[8817]: https://github.com/bevyengine/bevy/pull/8817 -[8818]: https://github.com/bevyengine/bevy/pull/8818 -[8822]: https://github.com/bevyengine/bevy/pull/8822 -[8826]: https://github.com/bevyengine/bevy/pull/8826 -[8832]: https://github.com/bevyengine/bevy/pull/8832 -[8833]: https://github.com/bevyengine/bevy/pull/8833 -[8834]: https://github.com/bevyengine/bevy/pull/8834 -[8843]: https://github.com/bevyengine/bevy/pull/8843 -[8844]: https://github.com/bevyengine/bevy/pull/8844 -[8845]: https://github.com/bevyengine/bevy/pull/8845 -[8848]: https://github.com/bevyengine/bevy/pull/8848 -[8849]: https://github.com/bevyengine/bevy/pull/8849 -[8852]: https://github.com/bevyengine/bevy/pull/8852 -[8866]: https://github.com/bevyengine/bevy/pull/8866 -[8868]: https://github.com/bevyengine/bevy/pull/8868 -[8871]: https://github.com/bevyengine/bevy/pull/8871 -[8877]: https://github.com/bevyengine/bevy/pull/8877 -[8878]: https://github.com/bevyengine/bevy/pull/8878 -[8886]: https://github.com/bevyengine/bevy/pull/8886 -[8890]: https://github.com/bevyengine/bevy/pull/8890 -[8891]: https://github.com/bevyengine/bevy/pull/8891 -[8901]: https://github.com/bevyengine/bevy/pull/8901 -[8903]: https://github.com/bevyengine/bevy/pull/8903 -[8904]: https://github.com/bevyengine/bevy/pull/8904 -[8905]: https://github.com/bevyengine/bevy/pull/8905 -[8907]: https://github.com/bevyengine/bevy/pull/8907 -[8909]: https://github.com/bevyengine/bevy/pull/8909 -[8910]: https://github.com/bevyengine/bevy/pull/8910 -[8920]: https://github.com/bevyengine/bevy/pull/8920 -[8928]: https://github.com/bevyengine/bevy/pull/8928 -[8933]: https://github.com/bevyengine/bevy/pull/8933 -[8939]: https://github.com/bevyengine/bevy/pull/8939 -[8947]: https://github.com/bevyengine/bevy/pull/8947 -[8951]: https://github.com/bevyengine/bevy/pull/8951 -[8960]: https://github.com/bevyengine/bevy/pull/8960 -[8957]: https://github.com/bevyengine/bevy/pull/8957 -[9054]: https://github.com/bevyengine/bevy/pull/9054 -[6690]: https://github.com/bevyengine/bevy/pull/6690 -[8424]: https://github.com/bevyengine/bevy/pull/8424 -[8655]: https://github.com/bevyengine/bevy/pull/8655 -[6793]: https://github.com/bevyengine/bevy/pull/6793 -[8720]: https://github.com/bevyengine/bevy/pull/8720 -[9024]: https://github.com/bevyengine/bevy/pull/9024 -[9027]: https://github.com/bevyengine/bevy/pull/9027 -[9016]: https://github.com/bevyengine/bevy/pull/9016 -[9023]: https://github.com/bevyengine/bevy/pull/9023 -[9020]: https://github.com/bevyengine/bevy/pull/9020 -[9030]: https://github.com/bevyengine/bevy/pull/9030 -[9013]: https://github.com/bevyengine/bevy/pull/9013 -[8926]: https://github.com/bevyengine/bevy/pull/8926 -[9003]: https://github.com/bevyengine/bevy/pull/9003 -[8993]: https://github.com/bevyengine/bevy/pull/8993 -[8508]: https://github.com/bevyengine/bevy/pull/8508 -[6056]: https://github.com/bevyengine/bevy/pull/6056 -[8987]: https://github.com/bevyengine/bevy/pull/8987 -[8952]: https://github.com/bevyengine/bevy/pull/8952 -[8961]: https://github.com/bevyengine/bevy/pull/8961 -[8978]: https://github.com/bevyengine/bevy/pull/8978 -[8982]: https://github.com/bevyengine/bevy/pull/8982 -[8977]: https://github.com/bevyengine/bevy/pull/8977 -[8931]: https://github.com/bevyengine/bevy/pull/8931 - -## Version 0.10.0 (2023-03-06) - -## Added - -- [Accessibility: Added `Label` for marking text specifically as a label for UI controls.][6874] -- [Accessibility: Integrate with and expose AccessKit accessibility.][6874] -- [App: `App::setup`][7586] -- [App: `SubApp::new`][7290] -- [App: Bevy apps will now log system information on startup by default][5454] -- [Audio Expose symphonia features from rodio in bevy_audio and bevy][6388] -- [Audio: Basic spatial audio][6028] -- [ECS: `bevy_ptr::dangling_with_align`: creates a well-aligned dangling pointer to a type whose alignment is not known at compile time.][6618] -- [ECS: `Column::get_added_ticks`][6547] -- [ECS: `Column::get_column_ticks`][6547] -- [ECS: `DetectChanges::set_if_neq`: triggering change detection when the new and previous values are equal. This will work on both components and resources.][6853] -- [ECS: `SparseSet::get_added_ticks`][6547] -- [ECS: `SparseSet::get_column_ticks`][6547] -- [ECS: `Tick`, a wrapper around a single change detection tick.][6547] -- [ECS: `UnsafeWorldCell::world_mut` now exists and can be used to get a `&mut World` out of `UnsafeWorldCell`][7381] -- [ECS: `WorldId` now implements the `FromWorld` trait.][7726] -- [ECS: A `core::fmt::Pointer` impl to `Ptr`, `PtrMut` and `OwnedPtr`.][6980] -- [ECS: Add `bevy_ecs::schedule_v3` module][6587] -- [ECS: Add `EntityMap::iter()`][6935] -- [ECS: Add `Ref` to the prelude][7392] -- [ECS: Add `report_sets` option to `ScheduleBuildSettings`][7756] -- [ECS: add `Resources::iter` to iterate over all resource IDs][6592] -- [ECS: add `UnsafeWorldCell` abstraction][6404] -- [ECS: Add `World::clear_resources` & `World::clear_all`][3212] -- [ECS: Add a basic example for system ordering][7017] -- [ECS: Add a missing impl of `ReadOnlySystemParam` for `Option>`][7245] -- [ECS: add a spawn_on_external method to allow spawning on the scope’s thread or an external thread][7415] -- [ECS: Add const `Entity::PLACEHOLDER`][6761] -- [ECS: Add example to show how to use `apply_system_buffers`][7793] -- [ECS: Add logging variants of system piping][6751] -- [ECS: Add safe constructors for untyped pointers `Ptr` and `PtrMut`][6539] -- [ECS: Add unit test with system that panics][7491] -- [ECS: Add wrapping_add to change_tick][7146] -- [ECS: Added “base sets” and ported CoreSet to use them.][7466] -- [ECS: Added `as_mut` and `as_ref` methods to `MutUntyped`.][7009] -- [ECS: Added `bevy::ecs::system::assert_is_read_only_system`.][7547] -- [ECS: Added `Components::resource_id`.][7284] -- [ECS: Added `DebugName` world query for more human friendly debug names of entities.][7186] -- [ECS: Added `distributive_run_if` to `IntoSystemConfigs` to enable adding a run condition to each system when using `add_systems`.][7724] -- [ECS: Added `EntityLocation::table_id`][6681] -- [ECS: Added `EntityLocation::table_row`.][6681] -- [ECS: Added `IntoIterator` implementation for `EventReader` so you can now do `&mut reader` instead of `reader.iter()` for events.][7720] -- [ECS: Added `len`, `is_empty`, `iter` methods on SparseSets.][7638] -- [ECS: Added `ManualEventReader::clear()`][7471] -- [ECS: Added `MutUntyped::with_type` which allows converting into a `Mut`][7113] -- [ECS: Added `new_for_test` on `ComponentInfo` to make test code easy.][7638] -- [ECS: Added `not` condition.][7559] -- [ECS: Added `on_timer` and `on_fixed_timer` run conditions][7866] -- [ECS: Added `OwningPtr::read_unaligned`.][7039] -- [ECS: Added `ReadOnlySystem`, which is implemented for any `System` type whose parameters all implement `ReadOnlySystemParam`.][7547] -- [ECS: Added `Ref` which allows inspecting change detection flags in an immutable way][7097] -- [ECS: Added `shrink` and `as_ref` methods to `PtrMut`.][7009] -- [ECS: Added `SystemMeta::name`][6900] -- [ECS: Added `SystemState::get_manual_mut`][7084] -- [ECS: Added `SystemState::get_manual`][7084] -- [ECS: Added `SystemState::update_archetypes`][7084] -- [ECS: Added a large number of methods on `App` to work with schedules ergonomically][7267] -- [ECS: Added conversions from `Ptr`, `PtrMut`, and `OwningPtr` to `NonNull`.][7181] -- [ECS: Added rore common run conditions: `on_event`, resource change detection, `state_changed`, `any_with_component`][7579] -- [ECS: Added support for variants of `bevy_ptr` types that do not require being correctly aligned for the pointee type.][7151] -- [ECS: Added the `CoreSchedule` enum][7267] -- [ECS: Added the `SystemParam` type `Deferred`, which can be used to defer `World` mutations. Powered by the new trait `SystemBuffer`.][6817] -- [ECS: Added the extension methods `.and_then(...)` and `.or_else(...)` to run conditions, which allows combining run conditions with short-circuiting behavior.][7605] -- [ECS: Added the marker trait `BaseSystemSet`, which is distinguished from a `FreeSystemSet`. These are both subtraits of `SystemSet`.][7863] -- [ECS: Added the method `reborrow` to `Mut`, `ResMut`, `NonSendMut`, and `MutUntyped`.][7114] -- [ECS: Added the private `prepare_view_uniforms` system now has a public system set for scheduling purposes, called `ViewSet::PrepareUniforms`][7267] -- [ECS: Added the trait `Combine`, which can be used with the new `CombinatorSystem` to create system combinators with custom behavior.][7605] -- [ECS: Added the trait `EntityCommand`. This is a counterpart of `Command` for types that execute code for a single entity.][7015] -- [ECS: introduce EntityLocation::INVALID const and adjust Entities::get comment][7623] -- [ECS: States derive macro][7535] -- [ECS: support for tuple structs and unit structs to the `SystemParam` derive macro.][6957] -- [Hierarchy: Add `Transform::look_to`][6692] -- [Hierarchy: Added `add_child`, `set_parent` and `remove_parent` to `EntityMut`][6926] -- [Hierarchy: Added `clear_children(&mut self) -> &mut Self` and `replace_children(&mut self, children: &[Entity]) -> &mut Self` function in `BuildChildren` trait][6035] -- [Hierarchy: Added `ClearChildren` and `ReplaceChildren` struct][6035] -- [Hierarchy: Added `push_and_replace_children_commands` and `push_and_clear_children_commands` test][6035] -- [Hierarchy: Added the `BuildChildrenTransformExt` trait][7024] -- [Input: add Input Method Editor support][7325] -- [Input: Added `Axis::devices`][5400] -- [INput: Added common run conditions for `bevy_input`][7806] -- [Macro: add helper for macro to get either bevy::x or bevy_x depending on how it was imported][7164] -- [Math: `CubicBezier2d`, `CubicBezier3d`, `QuadraticBezier2d`, and `QuadraticBezier3d` types with methods for sampling position, velocity, and acceleration. The generic `Bezier` type is also available, and generic over any degree of Bezier curve.][7653] -- [Math: `CubicBezierEasing`, with additional methods to allow for smooth easing animations.][7653] -- [Math: Added a generic cubic curve trait, and implementation for Cardinal splines (including Catmull-Rom), B-Splines, Beziers, and Hermite Splines. 2D cubic curve segments also implement easing functionality for animation.][7683] -- [New reflection path syntax: struct field access by index (example syntax: `foo#1`)][7321] -- [Reflect `State` generics other than just `RandomState` can now be reflected for both `hashbrown::HashMap` and `collections::HashMap`][7782] -- [Reflect: `Aabb` now implements `FromReflect`.][7396] -- [Reflect: `derive(Reflect)` now supports structs and enums that contain generic types][7364] -- [Reflect: `ParsedPath` for cached reflection paths][7321] -- [Reflect: `std::collections::HashMap` can now be reflected][7782] -- [Reflect: `std::collections::VecDeque` now implements `Reflect` and all relevant traits.][6831] -- [Reflect: Add reflection path support for `Tuple` types][7324] -- [Reflect: Added `ArrayIter::new`.][7449] -- [Reflect: Added `FromReflect::take_from_reflect`][6566] -- [Reflect: Added `List::insert` and `List::remove`.][7063] -- [Reflect: Added `Map::remove`][6564] -- [Reflect: Added `ReflectFromReflect`][6245] -- [Reflect: Added `TypeRegistrationDeserializer`, which simplifies getting a `&TypeRegistration` while deserializing a string.][7094] -- [Reflect: Added methods to `List` that were previously provided by `Array`][7467] -- [Reflect: Added support for enums in reflection paths][6560] -- [Reflect: Added the `bevy_reflect_compile_fail_tests` crate for testing compilation errors][7041] -- [Reflect: bevy_reflect: Add missing primitive registrations][7815] -- [Reflect: impl `Reflect` for `&'static Path`][6755] -- [Reflect: implement `Reflect` for `Fxaa`][7527] -- [Reflect: implement `TypeUuid` for primitives and fix multiple-parameter generics having the same `TypeUuid`][6633] -- [Reflect: Implemented `Reflect` + `FromReflect` for window events and related types. These types are automatically registered when adding the `WindowPlugin`.][6235] -- [Reflect: Register Hash for glam types][6786] -- [Reflect: Register missing reflected types for `bevy_render`][6811] -- [Render: A pub field `extras` to `GltfNode`/`GltfMesh`/`GltfPrimitive` which store extras][6973] -- [Render: A pub field `material_extras` to `GltfPrimitive` which store material extras][6973] -- [Render: Add 'Color::as_lcha' function (#7757)][7766] -- [Render: Add `Camera::viewport_to_world_2d`][6557] -- [Render: Add a more familiar hex color entry][7060] -- [Render: add ambient lighting hook][5428] -- [Render: Add bevy logo to the lighting example to demo alpha mask shadows][7895] -- [Render: Add Box::from_corners method][6672] -- [Render: add OpenGL and DX11 backends][7481] -- [Render: Add orthographic camera support back to directional shadows][7796] -- [Render: add standard material depth bias to pipeline][7847] -- [Render: Add support for Rgb9e5Ufloat textures][6781] -- [Render: Added buffer usage field to buffers][7423] -- [Render: can define a value from inside a shader][7518] -- [Render: EnvironmentMapLight support for WebGL2][7737] -- [Render: Implement `ReadOnlySystemParam` for `Extract<>`][7182] -- [Render: Initial tonemapping options][7594] -- [Render: ShaderDefVal: add an `UInt` option][6881] -- [Render: Support raw buffers in AsBindGroup macro][7701] -- [Rendering: `Aabb` now implements `Copy`.][7401] -- [Rendering: `ExtractComponent` can specify output type, and outputting is optional.][6699] -- [Rendering: `Mssaa::samples`][7292] -- [Rendering: Add `#else ifdef` to shader preprocessing.][7431] -- [Rendering: Add a field `push_constant_ranges` to RenderPipelineDescriptor and ComputePipelineDescriptor][7681] -- [Rendering: Added `Material::prepass_vertex_shader()` and `Material::prepass_fragment_shader()` to control the prepass from the `Material`][6284] -- [Rendering: Added `BloomSettings:lf_boost`, `BloomSettings:lf_boost_curvature`, `BloomSettings::high_pass_frequency` and `BloomSettings::composite_mode`.][6677] -- [Rendering: Added `BufferVec::extend`][6833] -- [Rendering: Added `BufferVec::truncate`][6833] -- [Rendering: Added `Camera::msaa_writeback` which can enable and disable msaa writeback.][7671] -- [Rendering: Added `CascadeShadowConfigBuilder` to help with creating `CascadeShadowConfig`][7456] -- [Rendering: Added `DepthPrepass` and `NormalPrepass` component to control which textures will be created by the prepass and available in later passes.][6284] -- [Rendering: Added `Draw::prepare` optional trait function.][6885] -- [Rendering: Added `DrawFunctionsInternals::id()`][6745] -- [Rendering: Added `FallbackImageCubemap`.][7051] -- [Rendering: Added `FogFalloff` enum for selecting between three widely used “traditional” fog falloff modes: `Linear`, `Exponential` and `ExponentialSquared`, as well as a more advanced `Atmospheric` fog;][6412] -- [Rendering: Added `get_input_node`][6720] -- [Rendering: Added `Lcha` member to `bevy_render::color::Color` enum][7483] -- [Rendering: Added `MainTaret::main_texture_other`][7343] -- [Rendering: Added `PhaseItem::entity`][6885] -- [Rendering: Added `prepass_enabled` flag to the `MaterialPlugin` that will control if a material uses the prepass or not.][6284] -- [Rendering: Added `prepass_enabled` flag to the `PbrPlugin` to control if the StandardMaterial uses the prepass. Currently defaults to false.][6284] -- [Rendering: Added `PrepassNode` that runs before the main pass][6284] -- [Rendering: Added `PrepassPlugin` to extract/prepare/queue the necessary data][6284] -- [Rendering: Added `RenderCommand::ItemorldQuery` associated type.][6885] -- [Rendering: Added `RenderCommand::ViewWorldQuery` associated type.][6885] -- [Rendering: Added `RenderContext::add_command_buffer`][7248] -- [Rendering: Added `RenderContext::begin_tracked_render_pass`.][7053] -- [Rendering: Added `RenderContext::finish`][7248] -- [Rendering: Added `RenderContext::new`][7248] -- [Rendering: Added `SortedCameras`, exposing information that was previously internal to the camera driver node.][7671] -- [Rendering: Added `try_add_node_edge`][6720] -- [Rendering: Added `try_add_slot_edge`][6720] -- [Rendering: Added `with_r`, `with_g`, `with_b`, and `with_a` to `Color`.][6899] -- [Rendering: Added 2x and 8x sample counts for MSAA.][7684] -- [Rendering: Added a `#[storage(index)]` attribute to the derive `AsBindGroup` macro.][6129] -- [Rendering: Added an `EnvironmentMapLight` camera component that adds additional ambient light to a scene.][7051] -- [Rendering: Added argument to `ScalingMode::WindowSize` that specifies the number of pixels that equals one world unit.][6201] -- [Rendering: Added cylinder shape][6809] -- [Rendering: Added example `shaders/texture_binding_array`.][6995] -- [Rendering: Added new capabilities for shader validation.][6995] -- [Rendering: Added specializable `BlitPipeline` and ported the upscaling node to use this.][7671] -- [Rendering: Added subdivisions field to shape::Plane][7546] -- [Rendering: Added support for additive and multiplicative blend modes in the PBR `StandardMaterial`, via `AlphaMode::Add` and `AlphaMode::Multiply`;][6644] -- [Rendering: Added support for distance-based fog effects for PBR materials, controllable per-camera via the new `FogSettings` component;][6412] -- [Rendering: Added support for KTX2 `R8_SRGB`, `R8_UNORM`, `R8G8_SRGB`, `R8G8_UNORM`, `R8G8B8_SRGB`, `R8G8B8_UNORM` formats by converting to supported wgpu formats as appropriate][4594] -- [Rendering: Added support for premultiplied alpha in the PBR `StandardMaterial`, via `AlphaMode::Premultiplied`;][6644] -- [Rendering: Added the ability to `#[derive(ExtractComponent)]` with an optional filter.][7399] -- [Rendering: Added: `bevy_render::color::LchRepresentation` struct][7483] -- [Rendering: Clone impl for MaterialPipeline][7548] -- [Rendering: Implemented `Clone` for all pipeline types.][6653] -- [Rendering: Smooth Transition between Animations][6922] -- [Support optional env variable `BEVY_ASSET_ROOT` to explicitly specify root assets directory.][5346] -- [Task: Add thread create/destroy callbacks to TaskPool][6561] -- [Tasks: Added `ThreadExecutor` that can only be ticked on one thread.][7087] -- [the extension methods `in_schedule(label)` and `on_startup()` for configuring the schedule a system belongs to.][7790] -- [Transform: Added `GlobalTransform::reparented_to`][7020] -- [UI: `Size::new` is now `const`][6602] -- [UI: Add const to methods and const defaults to bevy_ui][5542] -- [UI: Added `all`, `width` and `height` functions to `Size`.][7468] -- [UI: Added `Anchor` component to `Text2dBundle`][6807] -- [UI: Added `CalculatedSize::preserve_aspect_ratio`][6825] -- [UI: Added `Component` derive to `Anchor`][6807] -- [UI: Added `RelativeCursorPosition`, and an example showcasing it][7199] -- [UI: Added `Text::with_linebreak_behaviour`][7283] -- [UI: Added `TextBundle::with_linebreak_behaviour`][7283] -- [UI: Added a `BackgroundColor` component to `TextBundle`.][7596] -- [UI: Added a helper method `with_background_color` to `TextBundle`.][7596] -- [UI: Added the `SpaceEvenly` variant to `AlignContent`.][7859] -- [UI: Added the `Start` and `End` variants to `AlignItems`, `AlignSelf`, `AlignContent` and `JustifyContent`.][7859] -- [UI: Adds `flip_x` and `flip_y` fields to `ExtractedUiNode`.][6292] -- [Utils: Added `SyncCell::read`, which allows shared access to values that already implement the `Sync` trait.][7718] -- [Utils: Added the guard type `bevy_utils::OnDrop`.][7181] -- [Window: Add `Windows::get_focused(_mut)`][6571] -- [Window: add span to winit event handler][6612] -- [Window: Transparent window on macos][7617] -- [Windowing: `WindowDescriptor` renamed to `Window`.][5589] -- [Windowing: Added `hittest` to `WindowAttributes`][6664] -- [Windowing: Added `Window::prevent_default_event_handling` . This allows bevy apps to not override default browser behavior on hotkeys like F5, F12, Ctrl+R etc.][7304] -- [Windowing: Added `WindowDescriptor.always_on_top` which configures a window to stay on top.][6527] -- [Windowing: Added an example `cargo run --example fallthrough`][6664] -- [Windowing: Added the `hittest`’s setters/getters][6664] -- [Windowing: Modifed the `WindowDescriptor`’s `Default` impl.][6664] -- [Windowing: Modified the `WindowBuilder`][6664] - -## Changed - -- [Animation: `AnimationPlayer` that are on a child or descendant of another entity with another player will no longer be run.][6785] -- [Animation: Animation sampling now runs fully multi-threaded using threads from `ComputeTaskPool`.][6785] -- [App: Adapt path type of dynamically_load_plugin][6734] -- [App: Break CorePlugin into TaskPoolPlugin, TypeRegistrationPlugin, FrameCountPlugin.][7083] -- [App: Increment FrameCount in CoreStage::Last.][7477] -- [App::run() will now panic when called from Plugin::build()][4241] -- [Asset: `AssetIo::watch_path_for_changes` allows watched path and path to reload to differ][6797] -- [Asset: make HandleUntyped::id private][7076] -- [Audio: `AudioOutput` is now a `Resource`. It's no longer `!Send`][6436] -- [Audio: AudioOutput is actually a normal resource now, not a non-send resource][7262] -- [ECS: `.label(SystemLabel)` is now referred to as `.in_set(SystemSet)`][7267] -- [ECS: `App::add_default_labels` is now `App::add_default_sets`][7267] -- [ECS: `App::add_system_set` was renamed to `App::add_systems`][7267] -- [ECS: `Archetype` indices and `Table` rows have been newtyped as `ArchetypeRow` and `TableRow`.][4878] -- [ECS: `ArchetypeGeneration` now implements `Ord` and `PartialOrd`.][6742] -- [ECS: `bevy_pbr::add_clusters` is no longer an exclusive system][7267] -- [ECS: `Bundle::get_components` now takes a `FnMut(StorageType, OwningPtr)`. The provided storage type must be correct for the component being fetched.][6902] -- [ECS: `ChangeTrackers` has been deprecated. It will be removed in Bevy 0.11.][7306] -- [ECS: `Command` closures no longer need to implement the marker trait `std::marker::Sync`.][7014] -- [ECS: `CoreStage` and `StartupStage` enums are now `CoreSet` and `StartupSet`][7267] -- [ECS: `EntityMut::world_scope` now allows returning a value from the immediately-computed closure.][7385] -- [ECS: `EntityMut`: rename `remove_intersection` to `remove` and `remove` to `take`][7810] -- [ECS: `EventReader::clear` now takes a mutable reference instead of consuming the event reader.][6851] -- [ECS: `EventWriter::send_batch` will only log a TRACE level log if the batch is non-empty.][7753] -- [ECS: `oldest_id` and `get_event` convenience methods added to `Events`.][5735] -- [ECS: `OwningPtr::drop_as` will now panic in debug builds if the pointer is not aligned.][7117] -- [ECS: `OwningPtr::read` will now panic in debug builds if the pointer is not aligned.][7117] -- [ECS: `Ptr::deref` will now panic in debug builds if the pointer is not aligned.][7117] -- [ECS: `PtrMut::deref_mut` will now panic in debug builds if the pointer is not aligned.][7117] -- [ECS: `Query::par_for_each(_mut)` has been changed to `Query::par_iter(_mut)` and will now automatically try to produce a batch size for callers based on the current `World` state.][4777] -- [ECS: `RemovedComponents` now internally uses an `Events` instead of an `Events`][7503] -- [ECS: `SceneSpawnerSystem` now runs under `CoreSet::Update`, rather than `CoreStage::PreUpdate.at_end()`.][7267] -- [ECS: `StartupSet` is now a base set][7574] -- [ECS: `System::default_labels` is now `System::default_system_sets`.][7267] -- [ECS: `SystemLabel` trait was replaced by `SystemSet`][7267] -- [ECS: `SystemParamState::apply` now takes a `&SystemMeta` parameter in addition to the provided `&mut World`.][6900] -- [ECS: `SystemTypeIdLabel` was replaced by `SystemSetType`][7267] -- [ECS: `tick_global_task_pools_on_main_thread` is no longer run as an exclusive system. Instead, it has been replaced by `tick_global_task_pools`, which uses a `NonSend` resource to force running on the main thread.][7267] -- [ECS: `Tick::is_older_than` was renamed to `Tick::is_newer_than`. This is not a functional change, since that was what was always being calculated, despite the wrong name.][7561] -- [ECS: `UnsafeWorldCell::world` is now used to get immutable access to the whole world instead of just the metadata which can now be done via `UnsafeWorldCell::world_metadata`][7381] -- [ECS: `World::init_non_send_resource` now returns the generated `ComponentId`.][7284] -- [ECS: `World::init_resource` now returns the generated `ComponentId`.][7284] -- [ECS: `World::iter_entities` now returns an iterator of `EntityRef` instead of `Entity`.][6843] -- [ECS: `World`s can now only hold a maximum of 2^32 - 1 tables.][6681] -- [ECS: `World`s can now only hold a maximum of 2^32- 1 archetypes.][6681] -- [ECS: `WorldId` now implements `SystemParam` and will return the id of the world the system is running in][7741] -- [ECS: Adding rendering extraction systems now panics rather than silently failing if no subapp with the `RenderApp` label is found.][7267] -- [ECS: Allow adding systems to multiple sets that share the same base set][7709] -- [ECS: change `is_system_type() -> bool` to `system_type() -> Option`][7715] -- [ECS: changed some `UnsafeWorldCell` methods to take `self` instead of `&self`/`&mut self` since there is literally no point to them doing that][7381] -- [ECS: Changed: `Query::for_each(_mut)`, `QueryParIter` will now leverage autovectorization to speed up query iteration where possible.][6547] -- [ECS: Default to using ExecutorKind::SingleThreaded on wasm32][7717] -- [ECS: Ensure `Query` does not use the wrong `World`][7150] -- [ECS: Exclusive systems may now be used with system piping.][7023] -- [ECS: expose `ScheduleGraph` for use in third party tools][7522] -- [ECS: extract topsort logic to a new method, one pass to detect cycles and …][7727] -- [ECS: Fixed time steps now use a schedule (`CoreSchedule::FixedTimeStep`) rather than a run criteria.][7267] -- [ECS: for disconnected, use Vec instead of HashSet to reduce insert overhead][7744] -- [ECS: Implement `SparseSetIndex` for `WorldId`][7125] -- [ECS: Improve the panic message for schedule build errors][7860] -- [ECS: Lift the 16-field limit from the `SystemParam` derive][6867] -- [ECS: Make `EntityRef::new` unsafe][7222] -- [ECS: Make `Query` fields private][7149] -- [ECS: make `ScheduleGraph::initialize` public][7723] -- [ECS: Make boxed conditions read-only][7786] -- [ECS: Make RemovedComponents mirror EventReaders api surface][7713] -- [ECS: Mark TableRow and TableId as repr(transparent)][7166] -- [ECS: Most APIs returning `&UnsafeCell` now returns `TickCells` instead, which contains two separate `&UnsafeCell` for either component ticks.][6547] -- [ECS: Move MainThreadExecutor for stageless migration.][7444] -- [ECS: Move safe operations out of `unsafe` blocks in `Query`][7851] -- [ECS: Optimize `.nth()` and `.last()` for event iterators][7530] -- [ECS: Optimize `Iterator::count` for event iterators][7582] -- [ECS: Provide public `EntityRef::get_change_ticks_by_id` that takes `ComponentId`][6683] -- [ECS: refactor: move internals from `entity_ref` to `World`, add `SAFETY` comments][6402] -- [ECS: Rename `EntityId` to `EntityIndex`][6732] -- [ECS: Rename `UnsafeWorldCellEntityRef` to `UnsafeEntityCell`][7568] -- [ECS: Rename schedule v3 to schedule][7519] -- [ECS: Rename state_equals condition to in_state][7677] -- [ECS: Replace `World::read_change_ticks` with `World::change_ticks` within `bevy_ecs` crate][6816] -- [ECS: Replaced the trait `ReadOnlySystemParamFetch` with `ReadOnlySystemParam`.][6865] -- [ECS: Simplified the `SystemParamFunction` and `ExclusiveSystemParamFunction` traits.][7675] -- [ECS: Speed up `CommandQueue` by storing commands more densely][6391] -- [ECS: Stageless: move final apply outside of spawned executor][7445] -- [ECS: Stageless: prettier cycle reporting][7463] -- [ECS: Systems without `Commands` and `ParallelCommands` will no longer show a `system_commands` span when profiling.][6900] -- [ECS: The `ReportHierarchyIssue` resource now has a public constructor (`new`), and implements `PartialEq`][7267] -- [ECS: The `StartupSchedule` label is now defined as part of the `CoreSchedules` enum][7267] -- [ECS: The `SystemParam` derive is now more flexible, allowing you to omit unused lifetime parameters.][6694] -- [ECS: the top level `bevy_ecs::schedule` module was replaced with `bevy_ecs::scheduling`][7267] -- [ECS: Use `World` helper methods for sending `HierarchyEvent`s][6921] -- [ECS: Use a bounded channel in the multithreaded executor][7829] -- [ECS: Use a default implementation for `set_if_neq`][7660] -- [ECS: Use consistent names for marker generics][7788] -- [ECS: Use correct terminology for a `NonSend` run condition panic][7841] -- [ECS: Use default-implemented methods for `IntoSystemConfig<>`][7870] -- [ECS: use try_send to replace send.await, unbounded channel should always b…][7745] -- [General: The MSRV of the engine is now 1.67.][7379] -- [Input: Bump gilrs version to 0.10][6558] -- [IOS, Android... same thing][7493] -- [Math: Update `glam` to `0.23`][7883] -- [Math: use `Mul` to double the value of `Vec3`][6607] -- [Reflect: bevy_reflect now uses a fixed state for its hasher, which means the output of `Reflect::reflect_hash` is now deterministic across processes.][7583] -- [Reflect: Changed function signatures of `ReflectComponent` methods, `apply`, `remove`, `contains`, and `reflect`.][7206] -- [Reflect: Changed the `List::push` and `List::pop` to have default implementations.][7063] -- [Reflect: Registered `SmallVec<[Entity; 8]>` in the type registry][6578] -- [Renamed methods on `GetPath`:][7321] - - `path` -> `reflect_path` - - `path_mut` -> `reflect_path_mut` - - `get_path` -> `path` - - `get_path_mut` -> `path_mut` -- [Render: Allow prepass in webgl][7537] -- [Render: bevy_pbr: Avoid copying structs and using registers in shaders][7069] -- [Render: bevy_pbr: Clear fog DynamicUniformBuffer before populating each frame][7432] -- [Render: bevy_render: Run calculate_bounds in the end-of-update exclusive systems][7127] -- [Render: Change the glTF loader to use `Camera3dBundle`][7890] -- [Render: Changed &mut PipelineCache to &PipelineCache][7598] -- [Render: Intepret glTF colors as linear instead of sRGB][6828] -- [Render: Move 'startup' Resource `WgpuSettings` into the `RenderPlugin`][6946] -- [Render: Move prepass functions to prepass_utils][7354] -- [Render: Only compute sprite color once per quad][7498] -- [Render: Only execute `#define` if current scope is accepting lines][7798] -- [Render: Pipelined Rendering][6503] -- [Render: Refactor Globals and View structs into separate shaders][7512] -- [Render: Replace UUID based IDs with a atomic-counted ones][6988] -- [Render: run clear trackers on render world][6878] -- [Render: set cull mode: None for Mesh2d][7514] -- [Render: Shader defs can now have a value][5900] -- [Render: Shrink ComputedVisibility][6305] -- [Render: Use prepass shaders for shadows][7784] -- [Rendering: `add_node_edge` is now infallible (panics on error)][6720] -- [Rendering: `add_slot_edge` is now infallible (panics on error)][6720] -- [Rendering: `AsBindGroup` is now object-safe.][6937] -- [Rendering: `BloomSettings::knee` renamed to `BloomPrefilterSettings::softness`.][6677] -- [Rendering: `BloomSettings::threshold` renamed to `BloomPrefilterSettings::threshold`.][6677] -- [Rendering: `HexColorError::Hex` has been renamed to `HexColorError::Char`][6940] -- [Rendering: `input_node` now panics on `None`][6720] -- [Rendering: `ktx2` and `zstd` are now part of bevy’s default enabled features][7696] -- [Rendering: `Msaa` is now enum][7292] -- [Rendering: `PipelineCache` no longer requires mutable access in order to queue render / compute pipelines.][7205] -- [Rendering: `RenderContext::command_encoder` is now private. Use the accessor `RenderContext::command_encoder()` instead.][7248] -- [Rendering: `RenderContext::render_device` is now private. Use the accessor `RenderContext::render_device()` instead.][7248] -- [Rendering: `RenderContext` now supports adding external `CommandBuffer`s for inclusion into the render graphs. These buffers can be encoded outside of the render graph (i.e. in a system).][7248] -- [Rendering: `scale` is now applied before updating `area`. Reading from it will take `scale` into account.][6201] -- [Rendering: `SkinnedMeshJoints::build` now takes a `&mut BufferVec` instead of a `&mut Vec` as a parameter.][6833] -- [Rendering: `StandardMaterial` now defaults to a dielectric material (0.0 `metallic`) with 0.5 `perceptual_roughness`.][7664] -- [Rendering: `TrackedRenderPass` now requires a `&RenderDevice` on construction.][7053] -- [Rendering: `Visibility` is now an enum][6320] -- [Rendering: Bloom now looks different.][6677] -- [Rendering: Directional lights now use cascaded shadow maps for improved shadow quality.][7064] -- [Rendering: ExtractedMaterials, extract_materials and prepare_materials are now public][7548] -- [Rendering: For performance reasons, some detailed renderer trace logs now require the use of cargo feature `detailed_trace` in addition to setting the log level to `TRACE` in order to be shown.][7639] -- [Rendering: Made cameras with the same target share the same `main_texture` tracker, which ensures continuity across cameras.][7671] -- [Rendering: Renamed `ScalingMode::Auto` to `ScalingMode::AutoMin`.][6496] -- [Rendering: Renamed `ScalingMode::None` to `ScalingMode::Fixed`][6201] -- [Rendering: Renamed `window_origin` to `viewport_origin`][6201] -- [Rendering: Renamed the `priority` field on `Camera` to `order`.][6908] -- [Rendering: Replaced `left`, `right`, `bottom`, and `top` fields with a single `area: Rect`][6201] -- [Rendering: StandardMaterials will now appear brighter and more saturated at high roughness, due to internal material changes. This is more physically correct.][7051] -- [Rendering: The `layout` field of `RenderPipelineDescriptor` and `ComputePipelineDescriptor` is now mandatory.][7681] -- [Rendering: The `rangefinder` module has been moved into the `render_phase` module.][7016] -- [Rendering: The bloom example has been renamed to bloom_3d and improved. A bloom_2d example was added.][6677] -- [Rendering: the SubApp Extract stage has been separated from running the sub app schedule.][7046] -- [Rendering: To enable multiple `RenderPhases` to share the same `TrackedRenderPass`, the `RenderPhase::render` signature has changed.][7043] -- [Rendering: update its `Transform` in order to preserve its `GlobalTransform` after the parent change][7024] -- [Rendering: Updated to wgpu 0.15, wgpu-hal 0.15.1, and naga 0.11][7356] -- [Rendering: Users can now use the DirectX Shader Compiler (DXC) on Windows with DX12 for faster shader compilation and ShaderModel 6.0+ support (requires `dxcompiler.dll` and `dxil.dll`)][7356] -- [Rendering: You can now set up the rendering code of a `RenderPhase` directly using the `RenderPhase::render` method, instead of implementing it manually in your render graph node.][7013] -- [Scenes: `SceneSpawner::spawn_dynamic` now returns `InstanceId` instead of `()`.][6663] -- [Shape: Change `From` to `TryFrom`][6484] -- [Tasks: `Scope` now uses `FallibleTask` to await the cancellation of all remaining tasks when it’s dropped.][6696] -- [Time: `Time::set_relative_speed_fXX` now allows a relative speed of -0.0.][7740] -- [UI: `FocusPolicy` default has changed from `FocusPolicy::Block` to `FocusPolicy::Pass`][7161] -- [UI: `TextPipeline::queue_text` and `GlyphBrush::compute_glyphs` now need a TextLineBreakBehaviour argument, in order to pass through the new field.][7283] -- [UI: `update_image_calculated_size_system` sets `preserve_aspect_ratio` to true for nodes with images.][6825] -- [UI: Added `Changed` to the change detection query of `text_system`. This ensures that any change in the size of a text node will cause any text it contains to be recomputed.][7674] -- [UI: Changed `Size::height` so it sets the `width` to `Val::AUTO`.][7626] -- [UI: Changed `Size::width` so it sets the `height` to `Val::AUTO`.][7626] -- [UI: Changed `TextAlignment` into an enum with `Left`, `Center`, and `Right` variants.][6807] -- [UI: Changed extract_uinodes to extract the flip_x and flip_y values from UiImage.][6292] -- [UI: Changed prepare_uinodes to swap the UV coordinates as required.][6292] -- [UI: Changed Taffy version to 0.3.3 and disabled its `grid` feature.][7859] -- [UI: Changed the `Size` `width` and `height` default values to `Val::Auto`][7475] -- [UI: Changed the `size` field of `CalculatedSize` to a Vec2.][7641] -- [UI: Changed UiImage derefs to texture field accesses.][6292] -- [UI: Changed UiImage to a struct with texture, flip_x, and flip_y fields.][6292] -- [UI: Modified the `text2d` example to show both linebreaking behaviours.][7283] -- [UI: Renamed `image_node_system` to `update_image_calculated_size_system`][6674] -- [UI: Renamed the `background_color` field of `ExtractedUiNode` to `color`.][7452] -- [UI: Simplified the UI examples. Replaced numeric values with the Flex property enums or elided them where possible, and removed the remaining use of auto margins.][7626] -- [UI: The `MeasureFunc` only preserves the aspect ratio when `preserve_aspect_ratio` is true.][6825] -- [UI: Updated `from_style` for Taffy 0.3.3.][7859] -- [UI: Upgraded to Taffy 0.2, improving UI layout performance significantly and adding the flexbox `gap` property and `AlignContent::SpaceEvenly`.][6743] -- [UI: Use `f32::INFINITY` instead of `f32::MAX` to represent unbounded text in Text2dBounds][6807] -- [Window: expose cursor position with scale][7297] -- [Window: Make WindowId::primary() const][6582] -- [Window: revert stage changed for window closing][7296] -- [Windowing: `WindowId` is now `Entity`.][5589] -- [Windowing: Moved `changed_window` and `despawn_window` systems to `CoreStage::Last` to avoid systems making changes to the `Window` between `changed_window` and the end of the frame as they would be ignored.][7517] -- [Windowing: Requesting maximization/minimization is done on the [`Window::state`] field.][5589] -- [Windowing: Width/height consolidated into a `WindowResolution` component.][5589] - -## Removed - -- [App: Removed `App::add_sub_app`][7290] -- [App: Rename dynamic feature][7340] -- [ECS: Remove .on_update method to improve API consistency and clarity][7667] -- [ECS: Remove `BuildWorldChildren` impl from `WorldChildBuilder`][6727] -- [ECS: Remove a duplicate lookup in `apply_state_transitions`][7800] -- [ECS: Remove an incorrect impl of `ReadOnlySystemParam` for `NonSendMut`][7243] -- [ECS: Remove APIs deprecated in 0.9][6801] -- [ECS: Remove broken `DoubleEndedIterator` impls on event iterators][7469] -- [ECS: Remove duplicate lookups from `Resource` initialization][7174] -- [ECS: Remove useless access to archetype in `UnsafeWorldCell::fetch_table`][7665] -- [ECS: Removed `AddBundle`. `Edges::get_add_bundle` now returns `Option`][6742] -- [ECS: Removed `Archetype::new` and `Archetype::is_empty`.][6742] -- [ECS: Removed `ArchetypeComponentId::new` and `ArchetypeComponentId::value`.][6742] -- [ECS: Removed `ArchetypeGeneration::value`][6742] -- [ECS: Removed `ArchetypeId::new` and `ArchetypeId::value`.][6742] -- [ECS: Removed `ArchetypeIdentity`.][6742] -- [ECS: Removed `Archetypes`’s `Default` implementation.][6742] -- [ECS: Removed `AsSystemLabel` trait][7267] -- [ECS: Removed `Entities::alloc_at_without_replacement` and `AllocAtWithoutReplacement`.][6740] -- [ECS: Removed `Entities`’s `Default` implementation.][6740] -- [ECS: Removed `EntityMeta`][6740] -- [ECS: Removed `on_hierarchy_reports_enabled` run criteria (now just uses an ad hoc resource checking run condition)][7267] -- [ECS: Removed `RunCriteriaLabel`][7267] -- [ECS: Removed `RunCriteriaLabel`][7267] -- [ECS: Removed `SystemParamFetch`, its functionality has been moved to `SystemParamState`.][6865] -- [ECS: Removed `Table::component_capacity`][4928] -- [ECS: Removed `transform_propagate_system_set`: this was a nonstandard pattern that didn’t actually provide enough control. The systems are already `pub`: the docs have been updated to ensure that the third-party usage is clear.][7267] -- [ECS: removed `UnsafeWorldCell::storages` since that is probably unsound since storages contains the actual component/resource data not just metadata][7381] -- [ECS: Removed stages, and all code that mentions stages][7267] -- [ECS: Removed states have been dramatically simplified, and no longer use a stack][7267] -- [ECS: Removed systems in `RenderSet/Stage::Extract` no longer warn when they do not read data from the main world][7267] -- [ECS: Removed the bound `T: Sync` from `Local` when used as an `ExclusiveSystemParam`.][7040] -- [ECS: Removed the method `ExclusiveSystemParamState::apply`.][7489] -- [ECS: Removed the trait `ExclusiveSystemParamState`, merging its functionality into `ExclusiveSystemParam`.][6919] -- [ECS: Removed the trait `SystemParamState`, merging its functionality into `SystemParam`.][6919] -- [ECS: Support `SystemParam` types with const generics][7001] -- [ECS: Use T::Storage::STORAGE_TYPE to optimize out unused branches][6800] -- [Hierarchy: Expose transform propagate systems][7145] -- [Hierarchy: Make adding children idempotent][6763] -- [Hierarchy: Remove `EntityCommands::add_children`][6942] -- [Input: Gamepad events refactor][6965] -- [Reflect: Make proc macros hygienic in bevy_reflect_derive][6752] -- [Reflect: Removed `#[module]` helper attribute for `Reflect` derives (this is not currently used)][7148] -- [Reflect: Removed `Array` as supertrait of `List`][7467] -- [Reflect: Removed `PixelInfo` and get `pixel_size` from wgpu][6820] -- [Reflect: Removed `ReflectSerialize` and `ReflectDeserialize` registrations from most glam types][6580] -- [Remove unnecessary `Default` impl of HandleType][7472] -- [Remove warning about missed events due to false positives][6730] -- [Render: Make Core Pipeline Graph Nodes Public][6605] -- [Render: Optimize color computation in prepare_uinodes][7311] -- [Render: Organized scene_viewer into plugins for reuse and organization][6936] -- [Render: put `update_frusta::` in `UpdateProjectionFrusta` set][7526] -- [Render: Remove dependency on the mesh struct in the pbr function][7597] -- [Render: remove potential ub in render_resource_wrapper][7279] -- [Render: Remove redundant bitwise OR `TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES`][7033] -- [Render: Remove the early exit to make sure the prepass textures are cleared][7891] -- [Render: remove the image loaded check for nodes without images in extract_uinodes][7280] -- [Render: Remove unnecessary alternate create_texture path in prepare_asset for Image][6671] -- [Render: remove unused var in fxaa shader][7509] -- [Render: set AVAILABLE_STORAGE_BUFFER_BINDINGS to the actual number of buffers available][6787] -- [Render: Use `Time` `resource` instead of `Extract`ing `Time`][7316] -- [Render: use better set inheritance in render systems][7524] -- [Render: use blendstate blend for alphamode::blend][7899] -- [Render: Use Image::default for 1 pixel white texture directly][7884] -- [Rendering: Removed `bevy_render::render_phase::DrawState`. It was not usable in any form outside of `bevy_render`.][7053] -- [Rendering: Removed `BloomSettings::scale`.][6677] -- [Rendering: Removed `EntityPhaseItem` trait][6885] -- [Rendering: Removed `ExtractedJoints`.][6833] -- [Rendering: Removed `SetShadowViewBindGroup`, `queue_shadow_view_bind_group()`, and `LightMeta::shadow_view_bind_group` in favor of reusing the prepass view bind group.][7875] -- [Rendering: Removed the `render` feature group.][6912] -- [Scene: scene viewer: can select a scene from the asset path][6859] -- [Text: Warn instead of erroring when max_font_atlases is exceeded][6673] -- [Transform: Removed `GlobalTransform::translation_mut`][7134] -- [UI: Re-enable taffy send+sync assert][7769] -- [UI: Remove `TextError::ExceedMaxTextAtlases(usize)` variant][6796] -- [UI: Remove needless manual default impl of ButtonBundle][6970] -- [UI: Removed `HorizontalAlign` and `VerticalAlign`.][6807] -- [UI: Removed `ImageMode`.][6674] -- [UI: Removed `QueuedText`][7414] -- [UI: Removed the `image_mode` field from `ImageBundle`][6674] -- [UI: Removed the `Val` <-> `f32` conversion for `CalculatedSize`.][7641] -- [Update toml_edit to 0.18][7370] -- [Update tracing-chrome requirement from 0.6.0 to 0.7.0][6709] -- [Window: Remove unnecessary windows.rs file][7277] -- [Windowing: `window.always_on_top` has been removed, you can now use `window.window_level`][7480] -- [Windowing: Removed `ModifiesWindows` system label.][7517] - -## Fixed - -- [Asset: Fix asset_debug_server hang. There should be at most one ThreadExecut…][7825] -- [Asset: fix load_internal_binary_asset with debug_asset_server][7246] -- [Assets: Hot reloading for `LoadContext::read_asset_bytes`][6797] -- [Diagnostics: Console log messages now show when the `trace_tracy` feature was enabled.][6955] -- [ECS: Fix `last_changed()` and `set_last_changed()` for `MutUntyped`][7619] -- [ECS: Fix a miscompilation with `#[derive(SystemParam)]`][7105] -- [ECS: Fix get_unchecked_manual using archetype index instead of table row.][6625] -- [ECS: Fix ignored lifetimes in `#[derive(SystemParam)]`][7458] -- [ECS: Fix init_non_send_resource overwriting previous values][7261] -- [ECS: fix mutable aliases for a very short time if `WorldCell` is already borrowed][6639] -- [ECS: Fix partially consumed `QueryIter` and `QueryCombinationIter` having invalid `size_hint`][5214] -- [ECS: Fix PipeSystem panicking with exclusive systems][6698] -- [ECS: Fix soundness bug with `World: Send`. Dropping a `World` that contains a `!Send` resource on the wrong thread will now panic.][6534] -- [ECS: Fix Sparse Change Detection][6896] -- [ECS: Fix trait bounds for run conditions][7688] -- [ECS: Fix unsoundnes in `insert` `remove` and `despawn`][7805] -- [ECS: Fix unsoundness in `EntityMut::world_scope`][7387] -- [ECS: Fixed `DetectChanges::last_changed` returning the wrong value.][7560] -- [ECS: Fixed `DetectChangesMut::set_last_changed` not actually updating the `changed` tick.][7560] -- [ECS: Fixed `Res` and `Query` parameter never being mutually exclusive.][5105] -- [ECS: Fixed a bug that caused `#[derive(SystemParam)]` to leak the types of private fields.][7056] -- [ECS: schedule_v3: fix default set for systems not being applied][7350] -- [ECS: Stageless: close the finish channel so executor doesn't deadlock][7448] -- [ECS: Stageless: fix unapplied systems][7446] -- [Hierarchy: don't error when sending HierarchyEvents when Event type not registered][7031] -- [Hierarchy: Fix unsoundness for `propagate_recursive`][7003] -- [Hierarchy: Fixed missing `ChildAdded` events][6926] -- [Input: Avoid triggering change detection for inputs][6847] -- [Input: Fix `AxisSettings::new` only accepting invalid bounds][7233] -- [Input: Fix incorrect behavior of `just_pressed` and `just_released` in `Input`][7238] -- [Input: Removed Mobile Touch event y-axis flip][6597] -- [Reflect: bevy_reflect: Fix misplaced impls][6829] -- [Reflect: Fix bug where deserializing unit structs would fail for non-self-describing formats][6722] -- [Reflect: Fix bug where scene deserialization using certain readers could fail (e.g. `BufReader`, `File`, etc.)][6894] -- [Reflect: fix typo in bevy_reflect::impls::std GetTypeRegistration for vec like…][7520] -- [Reflect: Retain `::` after `>`, `)` or bracket when shortening type names][7755] -- [Render: bevy_core_pipeline: Fix prepass sort orders][7539] -- [Render: Cam scale cluster fix][7078] -- [Render: fix ambiguities in render schedule][7725] -- [Render: fix bloom viewport][6802] -- [Render: Fix dependency of shadow mapping on the optional `PrepassPlugin`][7878] -- [Render: Fix feature gating in texture_binding_array example][7425] -- [Render: Fix material alpha_mode in example global_vs_local_translation][6658] -- [Render: fix regex for shader define: must have at least one whitespace][7754] -- [Render: fix shader_instancing][7305] -- [Render: fix spot dir nan again][7176] -- [Render: Recreate tonemapping bind group if view uniforms buffer has changed][7904] -- [Render: Shadow render phase - pass the correct view entity][7048] -- [Render: Text2d doesn't recompute text on changes to the text's bounds][7846] -- [Render: wasm: pad globals uniform also in 2d][6643] -- [Rendering: Emission strength is now correctly interpreted by the `StandardMaterial` as linear instead of sRGB.][7897] -- [Rendering: Fix deband dithering intensity for non-HDR pipelines.][6707] -- [Rendering: Fixed StandardMaterial occlusion being incorrectly applied to direct lighting.][7051] -- [Rendering: Fixed the alpha channel of the `image::DynamicImage::ImageRgb32F` to `bevy_render::texture::Image` conversion in `bevy_render::texture::Image::from_dynamic()`.][6914] -- [Scene: Cleanup dynamic scene before building][6254] -- [Task: Fix panicking on another scope][6524] -- [UI: `Size::height` sets `width` not `height`][7478] -- [UI: Don't ignore UI scale for text][7510] -- [UI: Fix `bevy_ui` compile error without `bevy_text`][7877] -- [UI: Fix overflow scaling for images][7142] -- [UI: fix upsert_leaf not setting a MeasureFunc for new leaf nodes][7351] -- [Window: Apply `WindowDescriptor` settings in all modes][6934] -- [Window: break feedback loop when moving cursor][7298] -- [Window: create window as soon as possible][7668] -- [Window: Fix a typo on `Window::set_minimized`][7276] -- [Window: Fix closing window does not exit app in desktop_app mode][7628] -- [Window: fix cursor grab issue][7010] -- [Window: Fix set_cursor_grab_mode to try an alternative mode before giving an error][6599] - -[3212]: https://github.com/bevyengine/bevy/pull/3212 -[4241]: https://github.com/bevyengine/bevy/pull/4241 -[4594]: https://github.com/bevyengine/bevy/pull/4594 -[4777]: https://github.com/bevyengine/bevy/pull/4777 -[4878]: https://github.com/bevyengine/bevy/pull/4878 -[4928]: https://github.com/bevyengine/bevy/pull/4928 -[5105]: https://github.com/bevyengine/bevy/pull/5105 -[5214]: https://github.com/bevyengine/bevy/pull/5214 -[5346]: https://github.com/bevyengine/bevy/pull/5346 -[5400]: https://github.com/bevyengine/bevy/pull/5400 -[5428]: https://github.com/bevyengine/bevy/pull/5428 -[5454]: https://github.com/bevyengine/bevy/pull/5454 -[5542]: https://github.com/bevyengine/bevy/pull/5542 -[5589]: https://github.com/bevyengine/bevy/pull/5589 -[5735]: https://github.com/bevyengine/bevy/pull/5735 -[5900]: https://github.com/bevyengine/bevy/pull/5900 -[6028]: https://github.com/bevyengine/bevy/pull/6028 -[6035]: https://github.com/bevyengine/bevy/pull/6035 -[6129]: https://github.com/bevyengine/bevy/pull/6129 -[6201]: https://github.com/bevyengine/bevy/pull/6201 -[6235]: https://github.com/bevyengine/bevy/pull/6235 -[6245]: https://github.com/bevyengine/bevy/pull/6245 -[6254]: https://github.com/bevyengine/bevy/pull/6254 -[6284]: https://github.com/bevyengine/bevy/pull/6284 -[6292]: https://github.com/bevyengine/bevy/pull/6292 -[6305]: https://github.com/bevyengine/bevy/pull/6305 -[6320]: https://github.com/bevyengine/bevy/pull/6320 -[6388]: https://github.com/bevyengine/bevy/pull/6388 -[6391]: https://github.com/bevyengine/bevy/pull/6391 -[6402]: https://github.com/bevyengine/bevy/pull/6402 -[6404]: https://github.com/bevyengine/bevy/pull/6404 -[6412]: https://github.com/bevyengine/bevy/pull/6412 -[6436]: https://github.com/bevyengine/bevy/pull/6436 -[6484]: https://github.com/bevyengine/bevy/pull/6484 -[6496]: https://github.com/bevyengine/bevy/pull/6496 -[6503]: https://github.com/bevyengine/bevy/pull/6503 -[6524]: https://github.com/bevyengine/bevy/pull/6524 -[6527]: https://github.com/bevyengine/bevy/pull/6527 -[6534]: https://github.com/bevyengine/bevy/pull/6534 -[6539]: https://github.com/bevyengine/bevy/pull/6539 -[6547]: https://github.com/bevyengine/bevy/pull/6547 -[6557]: https://github.com/bevyengine/bevy/pull/6557 -[6558]: https://github.com/bevyengine/bevy/pull/6558 -[6560]: https://github.com/bevyengine/bevy/pull/6560 -[6561]: https://github.com/bevyengine/bevy/pull/6561 -[6564]: https://github.com/bevyengine/bevy/pull/6564 -[6566]: https://github.com/bevyengine/bevy/pull/6566 -[6571]: https://github.com/bevyengine/bevy/pull/6571 -[6578]: https://github.com/bevyengine/bevy/pull/6578 -[6580]: https://github.com/bevyengine/bevy/pull/6580 -[6582]: https://github.com/bevyengine/bevy/pull/6582 -[6587]: https://github.com/bevyengine/bevy/pull/6587 -[6592]: https://github.com/bevyengine/bevy/pull/6592 -[6597]: https://github.com/bevyengine/bevy/pull/6597 -[6599]: https://github.com/bevyengine/bevy/pull/6599 -[6602]: https://github.com/bevyengine/bevy/pull/6602 -[6605]: https://github.com/bevyengine/bevy/pull/6605 -[6607]: https://github.com/bevyengine/bevy/pull/6607 -[6612]: https://github.com/bevyengine/bevy/pull/6612 -[6618]: https://github.com/bevyengine/bevy/pull/6618 -[6625]: https://github.com/bevyengine/bevy/pull/6625 -[6633]: https://github.com/bevyengine/bevy/pull/6633 -[6639]: https://github.com/bevyengine/bevy/pull/6639 -[6643]: https://github.com/bevyengine/bevy/pull/6643 -[6644]: https://github.com/bevyengine/bevy/pull/6644 -[6653]: https://github.com/bevyengine/bevy/pull/6653 -[6658]: https://github.com/bevyengine/bevy/pull/6658 -[6663]: https://github.com/bevyengine/bevy/pull/6663 -[6664]: https://github.com/bevyengine/bevy/pull/6664 -[6671]: https://github.com/bevyengine/bevy/pull/6671 -[6672]: https://github.com/bevyengine/bevy/pull/6672 -[6673]: https://github.com/bevyengine/bevy/pull/6673 -[6674]: https://github.com/bevyengine/bevy/pull/6674 -[6677]: https://github.com/bevyengine/bevy/pull/6677 -[6681]: https://github.com/bevyengine/bevy/pull/6681 -[6683]: https://github.com/bevyengine/bevy/pull/6683 -[6692]: https://github.com/bevyengine/bevy/pull/6692 -[6694]: https://github.com/bevyengine/bevy/pull/6694 -[6696]: https://github.com/bevyengine/bevy/pull/6696 -[6698]: https://github.com/bevyengine/bevy/pull/6698 -[6699]: https://github.com/bevyengine/bevy/pull/6699 -[6707]: https://github.com/bevyengine/bevy/pull/6707 -[6709]: https://github.com/bevyengine/bevy/pull/6709 -[6720]: https://github.com/bevyengine/bevy/pull/6720 -[6722]: https://github.com/bevyengine/bevy/pull/6722 -[6727]: https://github.com/bevyengine/bevy/pull/6727 -[6730]: https://github.com/bevyengine/bevy/pull/6730 -[6732]: https://github.com/bevyengine/bevy/pull/6732 -[6734]: https://github.com/bevyengine/bevy/pull/6734 -[6740]: https://github.com/bevyengine/bevy/pull/6740 -[6742]: https://github.com/bevyengine/bevy/pull/6742 -[6743]: https://github.com/bevyengine/bevy/pull/6743 -[6745]: https://github.com/bevyengine/bevy/pull/6745 -[6751]: https://github.com/bevyengine/bevy/pull/6751 -[6752]: https://github.com/bevyengine/bevy/pull/6752 -[6755]: https://github.com/bevyengine/bevy/pull/6755 -[6761]: https://github.com/bevyengine/bevy/pull/6761 -[6763]: https://github.com/bevyengine/bevy/pull/6763 -[6781]: https://github.com/bevyengine/bevy/pull/6781 -[6785]: https://github.com/bevyengine/bevy/pull/6785 -[6786]: https://github.com/bevyengine/bevy/pull/6786 -[6787]: https://github.com/bevyengine/bevy/pull/6787 -[6796]: https://github.com/bevyengine/bevy/pull/6796 -[6797]: https://github.com/bevyengine/bevy/pull/6797 -[6800]: https://github.com/bevyengine/bevy/pull/6800 -[6801]: https://github.com/bevyengine/bevy/pull/6801 -[6802]: https://github.com/bevyengine/bevy/pull/6802 -[6807]: https://github.com/bevyengine/bevy/pull/6807 -[6809]: https://github.com/bevyengine/bevy/pull/6809 -[6811]: https://github.com/bevyengine/bevy/pull/6811 -[6816]: https://github.com/bevyengine/bevy/pull/6816 -[6817]: https://github.com/bevyengine/bevy/pull/6817 -[6820]: https://github.com/bevyengine/bevy/pull/6820 -[6825]: https://github.com/bevyengine/bevy/pull/6825 -[6828]: https://github.com/bevyengine/bevy/pull/6828 -[6829]: https://github.com/bevyengine/bevy/pull/6829 -[6831]: https://github.com/bevyengine/bevy/pull/6831 -[6833]: https://github.com/bevyengine/bevy/pull/6833 -[6843]: https://github.com/bevyengine/bevy/pull/6843 -[6847]: https://github.com/bevyengine/bevy/pull/6847 -[6851]: https://github.com/bevyengine/bevy/pull/6851 -[6853]: https://github.com/bevyengine/bevy/pull/6853 -[6859]: https://github.com/bevyengine/bevy/pull/6859 -[6865]: https://github.com/bevyengine/bevy/pull/6865 -[6867]: https://github.com/bevyengine/bevy/pull/6867 -[6874]: https://github.com/bevyengine/bevy/pull/6874 -[6878]: https://github.com/bevyengine/bevy/pull/6878 -[6881]: https://github.com/bevyengine/bevy/pull/6881 -[6885]: https://github.com/bevyengine/bevy/pull/6885 -[6894]: https://github.com/bevyengine/bevy/pull/6894 -[6896]: https://github.com/bevyengine/bevy/pull/6896 -[6899]: https://github.com/bevyengine/bevy/pull/6899 -[6900]: https://github.com/bevyengine/bevy/pull/6900 -[6902]: https://github.com/bevyengine/bevy/pull/6902 -[6908]: https://github.com/bevyengine/bevy/pull/6908 -[6912]: https://github.com/bevyengine/bevy/pull/6912 -[6914]: https://github.com/bevyengine/bevy/pull/6914 -[6919]: https://github.com/bevyengine/bevy/pull/6919 -[6921]: https://github.com/bevyengine/bevy/pull/6921 -[6922]: https://github.com/bevyengine/bevy/pull/6922 -[6926]: https://github.com/bevyengine/bevy/pull/6926 -[6934]: https://github.com/bevyengine/bevy/pull/6934 -[6935]: https://github.com/bevyengine/bevy/pull/6935 -[6936]: https://github.com/bevyengine/bevy/pull/6936 -[6937]: https://github.com/bevyengine/bevy/pull/6937 -[6940]: https://github.com/bevyengine/bevy/pull/6940 -[6942]: https://github.com/bevyengine/bevy/pull/6942 -[6946]: https://github.com/bevyengine/bevy/pull/6946 -[6955]: https://github.com/bevyengine/bevy/pull/6955 -[6957]: https://github.com/bevyengine/bevy/pull/6957 -[6965]: https://github.com/bevyengine/bevy/pull/6965 -[6970]: https://github.com/bevyengine/bevy/pull/6970 -[6973]: https://github.com/bevyengine/bevy/pull/6973 -[6980]: https://github.com/bevyengine/bevy/pull/6980 -[6988]: https://github.com/bevyengine/bevy/pull/6988 -[6995]: https://github.com/bevyengine/bevy/pull/6995 -[7001]: https://github.com/bevyengine/bevy/pull/7001 -[7003]: https://github.com/bevyengine/bevy/pull/7003 -[7009]: https://github.com/bevyengine/bevy/pull/7009 -[7010]: https://github.com/bevyengine/bevy/pull/7010 -[7013]: https://github.com/bevyengine/bevy/pull/7013 -[7014]: https://github.com/bevyengine/bevy/pull/7014 -[7015]: https://github.com/bevyengine/bevy/pull/7015 -[7016]: https://github.com/bevyengine/bevy/pull/7016 -[7017]: https://github.com/bevyengine/bevy/pull/7017 -[7020]: https://github.com/bevyengine/bevy/pull/7020 -[7023]: https://github.com/bevyengine/bevy/pull/7023 -[7024]: https://github.com/bevyengine/bevy/pull/7024 -[7031]: https://github.com/bevyengine/bevy/pull/7031 -[7033]: https://github.com/bevyengine/bevy/pull/7033 -[7039]: https://github.com/bevyengine/bevy/pull/7039 -[7040]: https://github.com/bevyengine/bevy/pull/7040 -[7041]: https://github.com/bevyengine/bevy/pull/7041 -[7043]: https://github.com/bevyengine/bevy/pull/7043 -[7046]: https://github.com/bevyengine/bevy/pull/7046 -[7048]: https://github.com/bevyengine/bevy/pull/7048 -[7051]: https://github.com/bevyengine/bevy/pull/7051 -[7053]: https://github.com/bevyengine/bevy/pull/7053 -[7056]: https://github.com/bevyengine/bevy/pull/7056 -[7060]: https://github.com/bevyengine/bevy/pull/7060 -[7063]: https://github.com/bevyengine/bevy/pull/7063 -[7064]: https://github.com/bevyengine/bevy/pull/7064 -[7069]: https://github.com/bevyengine/bevy/pull/7069 -[7076]: https://github.com/bevyengine/bevy/pull/7076 -[7078]: https://github.com/bevyengine/bevy/pull/7078 -[7083]: https://github.com/bevyengine/bevy/pull/7083 -[7084]: https://github.com/bevyengine/bevy/pull/7084 -[7087]: https://github.com/bevyengine/bevy/pull/7087 -[7094]: https://github.com/bevyengine/bevy/pull/7094 -[7097]: https://github.com/bevyengine/bevy/pull/7097 -[7105]: https://github.com/bevyengine/bevy/pull/7105 -[7113]: https://github.com/bevyengine/bevy/pull/7113 -[7114]: https://github.com/bevyengine/bevy/pull/7114 -[7117]: https://github.com/bevyengine/bevy/pull/7117 -[7125]: https://github.com/bevyengine/bevy/pull/7125 -[7127]: https://github.com/bevyengine/bevy/pull/7127 -[7134]: https://github.com/bevyengine/bevy/pull/7134 -[7142]: https://github.com/bevyengine/bevy/pull/7142 -[7145]: https://github.com/bevyengine/bevy/pull/7145 -[7146]: https://github.com/bevyengine/bevy/pull/7146 -[7148]: https://github.com/bevyengine/bevy/pull/7148 -[7149]: https://github.com/bevyengine/bevy/pull/7149 -[7150]: https://github.com/bevyengine/bevy/pull/7150 -[7151]: https://github.com/bevyengine/bevy/pull/7151 -[7161]: https://github.com/bevyengine/bevy/pull/7161 -[7164]: https://github.com/bevyengine/bevy/pull/7164 -[7166]: https://github.com/bevyengine/bevy/pull/7166 -[7174]: https://github.com/bevyengine/bevy/pull/7174 -[7176]: https://github.com/bevyengine/bevy/pull/7176 -[7181]: https://github.com/bevyengine/bevy/pull/7181 -[7182]: https://github.com/bevyengine/bevy/pull/7182 -[7186]: https://github.com/bevyengine/bevy/pull/7186 -[7199]: https://github.com/bevyengine/bevy/pull/7199 -[7205]: https://github.com/bevyengine/bevy/pull/7205 -[7206]: https://github.com/bevyengine/bevy/pull/7206 -[7222]: https://github.com/bevyengine/bevy/pull/7222 -[7233]: https://github.com/bevyengine/bevy/pull/7233 -[7238]: https://github.com/bevyengine/bevy/pull/7238 -[7243]: https://github.com/bevyengine/bevy/pull/7243 -[7245]: https://github.com/bevyengine/bevy/pull/7245 -[7246]: https://github.com/bevyengine/bevy/pull/7246 -[7248]: https://github.com/bevyengine/bevy/pull/7248 -[7261]: https://github.com/bevyengine/bevy/pull/7261 -[7262]: https://github.com/bevyengine/bevy/pull/7262 -[7267]: https://github.com/bevyengine/bevy/pull/7267 -[7276]: https://github.com/bevyengine/bevy/pull/7276 -[7277]: https://github.com/bevyengine/bevy/pull/7277 -[7279]: https://github.com/bevyengine/bevy/pull/7279 -[7280]: https://github.com/bevyengine/bevy/pull/7280 -[7283]: https://github.com/bevyengine/bevy/pull/7283 -[7284]: https://github.com/bevyengine/bevy/pull/7284 -[7290]: https://github.com/bevyengine/bevy/pull/7290 -[7292]: https://github.com/bevyengine/bevy/pull/7292 -[7296]: https://github.com/bevyengine/bevy/pull/7296 -[7297]: https://github.com/bevyengine/bevy/pull/7297 -[7298]: https://github.com/bevyengine/bevy/pull/7298 -[7304]: https://github.com/bevyengine/bevy/pull/7304 -[7305]: https://github.com/bevyengine/bevy/pull/7305 -[7306]: https://github.com/bevyengine/bevy/pull/7306 -[7311]: https://github.com/bevyengine/bevy/pull/7311 -[7316]: https://github.com/bevyengine/bevy/pull/7316 -[7321]: https://github.com/bevyengine/bevy/pull/7321 -[7324]: https://github.com/bevyengine/bevy/pull/7324 -[7325]: https://github.com/bevyengine/bevy/pull/7325 -[7340]: https://github.com/bevyengine/bevy/pull/7340 -[7343]: https://github.com/bevyengine/bevy/pull/7343 -[7350]: https://github.com/bevyengine/bevy/pull/7350 -[7351]: https://github.com/bevyengine/bevy/pull/7351 -[7354]: https://github.com/bevyengine/bevy/pull/7354 -[7356]: https://github.com/bevyengine/bevy/pull/7356 -[7364]: https://github.com/bevyengine/bevy/pull/7364 -[7370]: https://github.com/bevyengine/bevy/pull/7370 -[7379]: https://github.com/bevyengine/bevy/pull/7379 -[7381]: https://github.com/bevyengine/bevy/pull/7381 -[7385]: https://github.com/bevyengine/bevy/pull/7385 -[7387]: https://github.com/bevyengine/bevy/pull/7387 -[7392]: https://github.com/bevyengine/bevy/pull/7392 -[7396]: https://github.com/bevyengine/bevy/pull/7396 -[7399]: https://github.com/bevyengine/bevy/pull/7399 -[7401]: https://github.com/bevyengine/bevy/pull/7401 -[7414]: https://github.com/bevyengine/bevy/pull/7414 -[7415]: https://github.com/bevyengine/bevy/pull/7415 -[7423]: https://github.com/bevyengine/bevy/pull/7423 -[7425]: https://github.com/bevyengine/bevy/pull/7425 -[7431]: https://github.com/bevyengine/bevy/pull/7431 -[7432]: https://github.com/bevyengine/bevy/pull/7432 -[7444]: https://github.com/bevyengine/bevy/pull/7444 -[7445]: https://github.com/bevyengine/bevy/pull/7445 -[7446]: https://github.com/bevyengine/bevy/pull/7446 -[7448]: https://github.com/bevyengine/bevy/pull/7448 -[7449]: https://github.com/bevyengine/bevy/pull/7449 -[7452]: https://github.com/bevyengine/bevy/pull/7452 -[7456]: https://github.com/bevyengine/bevy/pull/7456 -[7458]: https://github.com/bevyengine/bevy/pull/7458 -[7463]: https://github.com/bevyengine/bevy/pull/7463 -[7466]: https://github.com/bevyengine/bevy/pull/7466 -[7467]: https://github.com/bevyengine/bevy/pull/7467 -[7468]: https://github.com/bevyengine/bevy/pull/7468 -[7469]: https://github.com/bevyengine/bevy/pull/7469 -[7471]: https://github.com/bevyengine/bevy/pull/7471 -[7472]: https://github.com/bevyengine/bevy/pull/7472 -[7475]: https://github.com/bevyengine/bevy/pull/7475 -[7477]: https://github.com/bevyengine/bevy/pull/7477 -[7478]: https://github.com/bevyengine/bevy/pull/7478 -[7480]: https://github.com/bevyengine/bevy/pull/7480 -[7481]: https://github.com/bevyengine/bevy/pull/7481 -[7483]: https://github.com/bevyengine/bevy/pull/7483 -[7489]: https://github.com/bevyengine/bevy/pull/7489 -[7491]: https://github.com/bevyengine/bevy/pull/7491 -[7493]: https://github.com/bevyengine/bevy/pull/7493 -[7498]: https://github.com/bevyengine/bevy/pull/7498 -[7503]: https://github.com/bevyengine/bevy/pull/7503 -[7509]: https://github.com/bevyengine/bevy/pull/7509 -[7510]: https://github.com/bevyengine/bevy/pull/7510 -[7512]: https://github.com/bevyengine/bevy/pull/7512 -[7514]: https://github.com/bevyengine/bevy/pull/7514 -[7517]: https://github.com/bevyengine/bevy/pull/7517 -[7518]: https://github.com/bevyengine/bevy/pull/7518 -[7519]: https://github.com/bevyengine/bevy/pull/7519 -[7520]: https://github.com/bevyengine/bevy/pull/7520 -[7522]: https://github.com/bevyengine/bevy/pull/7522 -[7524]: https://github.com/bevyengine/bevy/pull/7524 -[7526]: https://github.com/bevyengine/bevy/pull/7526 -[7527]: https://github.com/bevyengine/bevy/pull/7527 -[7530]: https://github.com/bevyengine/bevy/pull/7530 -[7535]: https://github.com/bevyengine/bevy/pull/7535 -[7537]: https://github.com/bevyengine/bevy/pull/7537 -[7539]: https://github.com/bevyengine/bevy/pull/7539 -[7546]: https://github.com/bevyengine/bevy/pull/7546 -[7547]: https://github.com/bevyengine/bevy/pull/7547 -[7548]: https://github.com/bevyengine/bevy/pull/7548 -[7559]: https://github.com/bevyengine/bevy/pull/7559 -[7560]: https://github.com/bevyengine/bevy/pull/7560 -[7561]: https://github.com/bevyengine/bevy/pull/7561 -[7568]: https://github.com/bevyengine/bevy/pull/7568 -[7574]: https://github.com/bevyengine/bevy/pull/7574 -[7579]: https://github.com/bevyengine/bevy/pull/7579 -[7582]: https://github.com/bevyengine/bevy/pull/7582 -[7583]: https://github.com/bevyengine/bevy/pull/7583 -[7586]: https://github.com/bevyengine/bevy/pull/7586 -[7594]: https://github.com/bevyengine/bevy/pull/7594 -[7596]: https://github.com/bevyengine/bevy/pull/7596 -[7597]: https://github.com/bevyengine/bevy/pull/7597 -[7598]: https://github.com/bevyengine/bevy/pull/7598 -[7605]: https://github.com/bevyengine/bevy/pull/7605 -[7617]: https://github.com/bevyengine/bevy/pull/7617 -[7619]: https://github.com/bevyengine/bevy/pull/7619 -[7623]: https://github.com/bevyengine/bevy/pull/7623 -[7626]: https://github.com/bevyengine/bevy/pull/7626 -[7628]: https://github.com/bevyengine/bevy/pull/7628 -[7638]: https://github.com/bevyengine/bevy/pull/7638 -[7639]: https://github.com/bevyengine/bevy/pull/7639 -[7641]: https://github.com/bevyengine/bevy/pull/7641 -[7653]: https://github.com/bevyengine/bevy/pull/7653 -[7660]: https://github.com/bevyengine/bevy/pull/7660 -[7664]: https://github.com/bevyengine/bevy/pull/7664 -[7665]: https://github.com/bevyengine/bevy/pull/7665 -[7667]: https://github.com/bevyengine/bevy/pull/7667 -[7668]: https://github.com/bevyengine/bevy/pull/7668 -[7671]: https://github.com/bevyengine/bevy/pull/7671 -[7674]: https://github.com/bevyengine/bevy/pull/7674 -[7675]: https://github.com/bevyengine/bevy/pull/7675 -[7677]: https://github.com/bevyengine/bevy/pull/7677 -[7681]: https://github.com/bevyengine/bevy/pull/7681 -[7683]: https://github.com/bevyengine/bevy/pull/7683 -[7684]: https://github.com/bevyengine/bevy/pull/7684 -[7688]: https://github.com/bevyengine/bevy/pull/7688 -[7696]: https://github.com/bevyengine/bevy/pull/7696 -[7701]: https://github.com/bevyengine/bevy/pull/7701 -[7709]: https://github.com/bevyengine/bevy/pull/7709 -[7713]: https://github.com/bevyengine/bevy/pull/7713 -[7715]: https://github.com/bevyengine/bevy/pull/7715 -[7717]: https://github.com/bevyengine/bevy/pull/7717 -[7718]: https://github.com/bevyengine/bevy/pull/7718 -[7720]: https://github.com/bevyengine/bevy/pull/7720 -[7723]: https://github.com/bevyengine/bevy/pull/7723 -[7724]: https://github.com/bevyengine/bevy/pull/7724 -[7725]: https://github.com/bevyengine/bevy/pull/7725 -[7726]: https://github.com/bevyengine/bevy/pull/7726 -[7727]: https://github.com/bevyengine/bevy/pull/7727 -[7737]: https://github.com/bevyengine/bevy/pull/7737 -[7740]: https://github.com/bevyengine/bevy/pull/7740 -[7741]: https://github.com/bevyengine/bevy/pull/7741 -[7744]: https://github.com/bevyengine/bevy/pull/7744 -[7745]: https://github.com/bevyengine/bevy/pull/7745 -[7753]: https://github.com/bevyengine/bevy/pull/7753 -[7754]: https://github.com/bevyengine/bevy/pull/7754 -[7755]: https://github.com/bevyengine/bevy/pull/7755 -[7756]: https://github.com/bevyengine/bevy/pull/7756 -[7766]: https://github.com/bevyengine/bevy/pull/7766 -[7769]: https://github.com/bevyengine/bevy/pull/7769 -[7782]: https://github.com/bevyengine/bevy/pull/7782 -[7784]: https://github.com/bevyengine/bevy/pull/7784 -[7786]: https://github.com/bevyengine/bevy/pull/7786 -[7788]: https://github.com/bevyengine/bevy/pull/7788 -[7790]: https://github.com/bevyengine/bevy/pull/7790 -[7793]: https://github.com/bevyengine/bevy/pull/7793 -[7796]: https://github.com/bevyengine/bevy/pull/7796 -[7798]: https://github.com/bevyengine/bevy/pull/7798 -[7800]: https://github.com/bevyengine/bevy/pull/7800 -[7805]: https://github.com/bevyengine/bevy/pull/7805 -[7806]: https://github.com/bevyengine/bevy/pull/7806 -[7810]: https://github.com/bevyengine/bevy/pull/7810 -[7815]: https://github.com/bevyengine/bevy/pull/7815 -[7825]: https://github.com/bevyengine/bevy/pull/7825 -[7829]: https://github.com/bevyengine/bevy/pull/7829 -[7841]: https://github.com/bevyengine/bevy/pull/7841 -[7846]: https://github.com/bevyengine/bevy/pull/7846 -[7847]: https://github.com/bevyengine/bevy/pull/7847 -[7851]: https://github.com/bevyengine/bevy/pull/7851 -[7859]: https://github.com/bevyengine/bevy/pull/7859 -[7860]: https://github.com/bevyengine/bevy/pull/7860 -[7863]: https://github.com/bevyengine/bevy/pull/7863 -[7866]: https://github.com/bevyengine/bevy/pull/7866 -[7870]: https://github.com/bevyengine/bevy/pull/7870 -[7875]: https://github.com/bevyengine/bevy/pull/7875 -[7877]: https://github.com/bevyengine/bevy/pull/7877 -[7878]: https://github.com/bevyengine/bevy/pull/7878 -[7883]: https://github.com/bevyengine/bevy/pull/7883 -[7884]: https://github.com/bevyengine/bevy/pull/7884 -[7890]: https://github.com/bevyengine/bevy/pull/7890 -[7891]: https://github.com/bevyengine/bevy/pull/7891 -[7895]: https://github.com/bevyengine/bevy/pull/7895 -[7897]: https://github.com/bevyengine/bevy/pull/7897 -[7899]: https://github.com/bevyengine/bevy/pull/7899 -[7904]: https://github.com/bevyengine/bevy/pull/7904 - -## Version 0.9.0 (2022-11-12) - -### Added - -- [Bloom][6397] -- [Add FXAA postprocessing][6393] -- [Fix color banding by dithering image before quantization][5264] -- [Plugins own their settings. Rework PluginGroup trait.][6336] -- [Add global time scaling][5752] -- [add globals to mesh view bind group][5409] -- [Add UI scaling][5814] -- [Add FromReflect for Timer][6422] -- [Re-add local bool `has_received_time` in `time_system`][6357] -- [Add default implementation of Serialize and Deserialize to Timer and Stopwatch][6248] -- [add time wrapping to Time][5982] -- [Stopwatch elapsed secs f64][5978] -- [Remaining fn in Timer][5971] -- [Support array / cubemap / cubemap array textures in KTX2][5325] -- [Add methods for silencing system-order ambiguity warnings][6158] -- [bevy_dynamic_plugin: make it possible to handle loading errors][6437] -- [can get the settings of a plugin from the app][6372] -- [Use plugin setup for resource only used at setup time][6360] -- [Add `TimeUpdateStrategy` resource for manual `Time` updating][6159] -- [dynamic scene builder][6227] -- [Create a scene from a dynamic scene][6229] -- [Scene example: write file in a task][5952] -- [Add writing of scene data to Scene example][5949] -- [can clone a scene][5855] -- [Add "end of main pass post processing" render graph node][6468] -- [Add `Camera::viewport_to_world`][6126] -- [Sprite: allow using a sub-region (Rect) of the image][6014] -- [Add missing type registrations for bevy_math types][5758] -- [Add `serialize` feature to `bevy_core`][6423] -- [add serialize feature to bevy_transform][6379] -- [Add associated constant `IDENTITY` to `Transform` and friends.][5340] -- [bevy_reflect: Add `Reflect::into_reflect`][6502] -- [Add reflect_owned][6494] -- [`Reflect` for `Tonemapping` and `ClusterConfig`][6488] -- [add `ReflectDefault` to std types][6429] -- [Add FromReflect for Visibility][6410] -- [Register `RenderLayers` type in `CameraPlugin`][6308] -- [Enable Constructing ReflectComponent/Resource][6257] -- [Support multiple `#[reflect]`/`#[reflect_value]` + improve error messages][6237] -- [Reflect Default for GlobalTransform][6200] -- [Impl Reflect for PathBuf and OsString][6193] -- [Reflect Default for `ComputedVisibility` and `Handle`][6187] -- [Register `Wireframe` type][6152] -- [Derive `FromReflect` for `Transform` and `GlobalTransform`][6015] -- [Make arrays behave like lists in reflection][5987] -- [Implement `Debug` for dynamic types][5948] -- [Implemented `Reflect` for all the ranges][5806] -- [Add `pop` method for `List` trait.][5797] -- [bevy_reflect: `GetTypeRegistration` for `SmallVec`][5782] -- [register missing reflect types][5747] -- [bevy_reflect: Get owned fields][5728] -- [bevy_reflect: Add `FromReflect` to the prelude][5720] -- [implement `Reflect` for `Input`, some misc improvements to reflect value derive][5676] -- [register `Cow<'static, str>` for reflection][5664] -- [bevy_reflect: Relax bounds on `Option`][5658] -- [remove `ReflectMut` in favor of `Mut`][5630] -- [add some info from `ReflectPathError` to the error messages][5626] -- [Added reflect/from reflect impls for NonZero integer types][5556] -- [bevy_reflect: Update enum derives][5473] -- [Add `reflect(skip_serializing)` which retains reflection but disables automatic serialization][5250] -- [bevy_reflect: Reflect enums][4761] -- [Disabling default features support in bevy_ecs, bevy_reflect and bevy][5993] -- [expose window alpha mode][6331] -- [Make bevy_window and bevy_input events serializable][6180] -- [Add window resizing example][5813] -- [feat: add GamepadInfo, expose gamepad names][6342] -- [Derive `Reflect` + `FromReflect` for input types][6232] -- [Make TouchInput and ForceTouch serializable][6191] -- [Add a Gamepad Viewer tool to examples][6074] -- [Derived `Copy` trait for `bevy_input` events, `Serialize`/`Deserialize` for events in `bevy_input` and `bevy_windows`, `PartialEq` for events in both, and `Eq` where possible in both.][6023] -- [Support for additional gamepad buttons and axis][5853] -- [Added keyboard scan input event][5495] -- [Add `set_parent` and `remove_parent` to `EntityCommands`][6189] -- [Add methods to `Query<&Children>` and `Query<&Parent>` to iterate over descendants and ancestors][6185] -- [Add `is_finished` to `Task`][6444] -- [Expose mint feature in bevy_math/glam][5857] -- [Utility methods for Val][6134] -- [Register missing bevy_text types][6029] -- [Add additional constructors for `UiRect` to specify values for specific fields][5988] -- [Add AUTO and UNDEFINED const constructors for `Size`][5761] -- [Add Exponential Moving Average into diagnostics][4992] -- [Add `send_event` and friends to `WorldCell`][6515] -- [Add a method for accessing the width of a `Table`][6249] -- [Add iter_entities to World #6228][6242] -- [Adding Debug implementations for App, Stage, Schedule, Query, QueryState, etc.][6214] -- [Add a method for mapping `Mut` -> `Mut`][6199] -- [implemented #[bundle(ignore)]][6123] -- [Allow access to non-send resource through `World::resource_scope`][6113] -- [Add get_entity to Commands][5854] -- [Added the ability to get or set the last change tick of a system.][5838] -- [Add a module for common system `chain`/`pipe` adapters][5776] -- [SystemParam for the name of the system you are currently in][5731] -- [Warning message for missing events][5730] -- [Add a change detection bypass and manual control over change ticks][5635] -- [Add into_world_mut to EntityMut][5586] -- [Add `FromWorld` bound to `T` in `Local`][5481] -- [Add `From` for EntityRef (fixes #5459)][5461] -- [Implement IntoIterator for ECS wrapper types.][5096] -- [add `Res::clone`][4109] -- [Add CameraRenderGraph::set][6470] -- [Use wgsl saturate][6318] -- [Add mutating `toggle` method to `Visibility` component][6268] -- [Add globals struct to mesh2d][6222] -- [add support for .comp glsl shaders][6084] -- [Implement `IntoIterator` for `&Extract

`][6025] -- [add Debug, Copy, Clone derives to Circle][6009] -- [Add TextureFormat::Rg16Unorm support for Image and derive Resource for SpecializedComputePipelines][5991] -- [Add `bevy_render::texture::ImageSettings` to prelude][5566] -- [Add `Projection` component to prelude.][5557] -- [Expose `Image` conversion functions (fixes #5452)][5527] -- [Macro for Loading Internal Binary Assets][6478] -- [Add `From` for `AssetPath<'a>`][6337] -- [Add Eq & PartialEq to AssetPath][6274] -- [add `ReflectAsset` and `ReflectHandle`][5923] -- [Add warning when using load_folder on web][5827] -- [Expose rodio's Source and Sample traits in bevy_audio][6374] -- [Add a way to toggle `AudioSink`][6321] - -### Changed - -- [separate tonemapping and upscaling passes][3425] -- [Rework ViewTarget to better support post processing][6415] -- [bevy_reflect: Improve serialization format even more][5723] -- [bevy_reflect: Binary formats][6140] -- [Unique plugins][6411] -- [Support arbitrary RenderTarget texture formats][6380] -- [Make `Resource` trait opt-in, requiring `#[derive(Resource)]` V2][5577] -- [Replace `WorldQueryGats` trait with actual gats][6319] -- [Change UI coordinate system to have origin at top left corner][6000] -- [Move the cursor's origin back to the bottom-left][6533] -- [Add z-index support with a predictable UI stack][5877] -- [TaskPool Panic Handling][6443] -- [Implement `Bundle` for `Component`. Use `Bundle` tuples for insertion][2975] -- [Spawn now takes a Bundle][6054] -- [make `WorldQuery` very flat][5205] -- [Accept Bundles for insert and remove. Deprecate insert/remove_bundle][6039] -- [Exclusive Systems Now Implement `System`. Flexible Exclusive System Params][6083] -- [bevy_scene: Serialize entities to map][6416] -- [bevy_scene: Stabilize entity order in `DynamicSceneBuilder`][6382] -- [bevy_scene: Replace root list with struct][6354] -- [bevy_scene: Use map for scene `components`][6345] -- [Start running systems while prepare_systems is running][4919] -- [Extract Resources into their own dedicated storage][4809] -- [get proper texture format after the renderer is initialized, fix #3897][5413] -- [Add getters and setters for `InputAxis` and `ButtonSettings`][6088] -- [Clean up Fetch code][4800] -- [Nested spawns on scope][4466] -- [Skip empty archetypes and tables when iterating over queries][4724] -- [Increase the `MAX_DIRECTIONAL_LIGHTS` from 1 to 10][6066] -- [bevy_pbr: Normalize skinned normals][6543] -- [remove mandatory mesh attributes][6127] -- [Rename `play` to `start` and add new `play` method that won't overwrite the existing animation if it's already playing][6350] -- [Replace the `bool` argument of `Timer` with `TimerMode`][6247] -- [improve panic messages for add_system_to_stage and add_system_set_to_stage][5847] -- [Use default serde impls for Entity][6194] -- [scenes: simplify return type of iter_instance_entities][5994] -- [Consistently use `PI` to specify angles in examples.][5825] -- [Remove `Transform::apply_non_uniform_scale`][6133] -- [Rename `Transform::mul_vec3` to `transform_point` and improve docs][6132] -- [make `register` on `TypeRegistry` idempotent][6487] -- [do not set cursor grab on window creation if not asked for][6381] -- [Make `raw_window_handle` field in `Window` and `ExtractedWindow` an `Option`.][6114] -- [Support monitor selection for all window modes.][5878] -- [`Gamepad` type is `Copy`; do not require / return references to it in `Gamepads` API][5296] -- [Update tracing-chrome to 0.6.0][6398] -- [Update to ron 0.8][5864] -- [Update clap requirement from 3.2 to 4.0][6303] -- [Update glam 0.22, hexasphere 8.0, encase 0.4][6427] -- [Update `wgpu` to 0.14.0, `naga` to `0.10.0`, `winit` to 0.27.4, `raw-window-handle` to 0.5.0, `ndk` to 0.7][6218] -- [Update to notify 5.0 stable][5865] -- [Update rodio requirement from 0.15 to 0.16][6020] -- [remove copyless][6100] -- [Mark `Task` as `#[must_use]`][6068] -- [Swap out num_cpus for std::thread::available_parallelism][4970] -- [Cleaning up NodeBundle, and some slight UI module re-organization][6473] -- [Make the default background color of `NodeBundle` transparent][6211] -- [Rename `UiColor` to `BackgroundColor`][6087] -- [changed diagnostics from seconds to milliseconds][5554] -- [Remove unnecesary branches/panics from Query accesses][6461] -- [`debug_checked_unwrap` should track its caller][6452] -- [Speed up `Query::get_many` and add benchmarks][6400] -- [Rename system chaining to system piping][6230] -- [[Fixes #6059] ``Entity``'s “ID” should be named “index” instead][6107] -- [`Query` filter types must be `ReadOnlyWorldQuery`][6008] -- [Remove ambiguity sets][5916] -- [relax `Sized` bounds around change detection types][5917] -- [Remove ExactSizeIterator from QueryCombinationIter][5895] -- [Remove Sync bound from Command][5871] -- [Make most `Entity` methods `const`][5688] -- [Remove `insert_resource_with_id`][5608] -- [Avoid making `Fetch`s `Clone`][5593] -- [Remove `Sync` bound from `Local`][5483] -- [Replace `many_for_each_mut` with `iter_many_mut`.][5402] -- [bevy_ecs: Use 32-bit entity ID cursor on platforms without AtomicI64][4452] -- [Specialize UI pipeline on "hdr-ness"][6459] -- [Allow passing `glam` vector types as vertex attributes][6442] -- [Add multi draw indirect draw calls][6392] -- [Take DirectionalLight's GlobalTransform into account when calculating shadow map volume (not just direction)][6384] -- [Respect mipmap_filter when create ImageDescriptor with linear()/nearest()][6349] -- [use bevy default texture format if the surface is not yet available][6233] -- [log pipeline cache errors earlier][6115] -- [Merge TextureAtlas::from_grid_with_padding into TextureAtlas::from_grid through option arguments][6057] -- [Reconfigure surface on present mode change][6049] -- [Use 3 bits of PipelineKey to store MSAA sample count][5826] -- [Limit FontAtlasSets][5708] -- [Move `sprite::Rect` into `bevy_math`][5686] -- [Make vertex colors work without textures in bevy_sprite][5685] -- [use bevy_default() for texture format in post_processing][5601] -- [don't render completely transparent UI nodes][5537] -- [make TextLayoutInfo a Component][4460] -- [make `Handle::` field id private, and replace with a getter][6176] -- [Remove `AssetServer::watch_for_changes()`][5968] -- [Rename Handle::as_weak() to cast_weak()][5321] -- [Remove `Sync` requirement in `Decodable::Decoder`][5819] - -### Fixed - -- [Optimize rendering slow-down at high entity counts][5509] -- [bevy_reflect: Fix `DynamicScene` not respecting component registrations during serialization][6288] -- [fixes the types for Vec3 and Quat in scene example to remove WARN from the logs][5751] -- [Fix end-of-animation index OOB][6210] -- [bevy_reflect: Remove unnecessary `Clone` bounds][5783] -- [bevy_reflect: Fix `apply` method for `Option`][5780] -- [Fix outdated and badly formatted docs for `WindowDescriptor::transparent`][6329] -- [disable window pre creation for ios][5883] -- [Remove unnecessary unsafe `Send` and `Sync` impl for `WinitWindows` on wasm.][5863] -- [Fix window centering when scale_factor is not 1.0][5582] -- [fix order of exit/close window systems][5558] -- [bevy_input: Fix process touch event][4352] -- [fix: explicitly specify required version of async-task][6509] -- [Fix `clippy::iter_with_drain`][6485] -- [Use `cbrt()` instead of `powf(1./3.)`][6481] -- [Fix `RemoveChildren` command][6192] -- [Fix inconsistent children removal behavior][6017] -- [tick local executor][6121] -- [Fix panic when the primary window is closed][6545] -- [UI scaling fix][6479] -- [Fix clipping in UI][6351] -- [Fixes scroll example after inverting UI Y axis][6290] -- [Fixes incorrect glyph positioning for text2d][6273] -- [Clean up taffy nodes when UI node entities are removed][5886] -- [Fix unsound `EntityMut::remove_children`. Add `EntityMut::world_scope`][6464] -- [Fix spawning empty bundles][6425] -- [Fix query.to_readonly().get_component_mut() soundness bug][6401] -- [#5817: derive_bundle macro is not hygienic][5835] -- [drop old value in `insert_resource_by_id` if exists][5587] -- [Fix lifetime bound on `From` impl for `NonSendMut` -> `Mut`][5560] -- [Fix `mesh.wgsl` error for meshes without normals][6439] -- [Fix panic when using globals uniform in wasm builds][6460] -- [Resolve most remaining execution-order ambiguities][6341] -- [Call `mesh2d_tangent_local_to_world` with the right arguments][6209] -- [Fixes Camera not being serializable due to missing registrations in core functionality.][6170] -- [fix spot dir nan bug][6167] -- [use alpha mask even when unlit][6047] -- [Ignore `Timeout` errors on Linux AMD & Intel][5957] -- [adjust cluster index for viewport origin][5947] -- [update camera projection if viewport changed][5945] -- [Ensure 2D phase items are sorted before batching][5942] -- [bevy_pbr: Fix incorrect and unnecessary normal-mapping code][5766] -- [Add explicit ordering between `update_frusta` and `camera_system`][5757] -- [bevy_pbr: Fix tangent and normal normalization][5666] -- [Fix shader syntax][5613] -- [Correctly use as_hsla_f32 in `Add` and `AddAssign`, fixes #5543][5546] -- [Sync up bevy_sprite and bevy_ui shader View struct][5531] -- [Fix View by adding missing fields present in ViewUniform][5512] -- [Freeing memory held by visible entities vector][3009] -- [Correctly parse labels with '#'][5729] - -[6545]: https://github.com/bevyengine/bevy/pull/6545 -[6543]: https://github.com/bevyengine/bevy/pull/6543 -[6533]: https://github.com/bevyengine/bevy/pull/6533 -[6515]: https://github.com/bevyengine/bevy/pull/6515 -[6509]: https://github.com/bevyengine/bevy/pull/6509 -[6502]: https://github.com/bevyengine/bevy/pull/6502 -[6494]: https://github.com/bevyengine/bevy/pull/6494 -[6488]: https://github.com/bevyengine/bevy/pull/6488 -[6487]: https://github.com/bevyengine/bevy/pull/6487 -[6485]: https://github.com/bevyengine/bevy/pull/6485 -[6481]: https://github.com/bevyengine/bevy/pull/6481 -[6479]: https://github.com/bevyengine/bevy/pull/6479 -[6478]: https://github.com/bevyengine/bevy/pull/6478 -[6473]: https://github.com/bevyengine/bevy/pull/6473 -[6470]: https://github.com/bevyengine/bevy/pull/6470 -[6468]: https://github.com/bevyengine/bevy/pull/6468 -[6464]: https://github.com/bevyengine/bevy/pull/6464 -[6461]: https://github.com/bevyengine/bevy/pull/6461 -[6460]: https://github.com/bevyengine/bevy/pull/6460 -[6459]: https://github.com/bevyengine/bevy/pull/6459 -[6452]: https://github.com/bevyengine/bevy/pull/6452 -[6444]: https://github.com/bevyengine/bevy/pull/6444 -[6443]: https://github.com/bevyengine/bevy/pull/6443 -[6442]: https://github.com/bevyengine/bevy/pull/6442 -[6439]: https://github.com/bevyengine/bevy/pull/6439 -[6437]: https://github.com/bevyengine/bevy/pull/6437 -[6429]: https://github.com/bevyengine/bevy/pull/6429 -[6427]: https://github.com/bevyengine/bevy/pull/6427 -[6425]: https://github.com/bevyengine/bevy/pull/6425 -[6423]: https://github.com/bevyengine/bevy/pull/6423 -[6422]: https://github.com/bevyengine/bevy/pull/6422 -[6416]: https://github.com/bevyengine/bevy/pull/6416 -[6415]: https://github.com/bevyengine/bevy/pull/6415 -[6411]: https://github.com/bevyengine/bevy/pull/6411 -[6410]: https://github.com/bevyengine/bevy/pull/6410 -[6401]: https://github.com/bevyengine/bevy/pull/6401 -[6400]: https://github.com/bevyengine/bevy/pull/6400 -[6398]: https://github.com/bevyengine/bevy/pull/6398 -[6397]: https://github.com/bevyengine/bevy/pull/6397 -[6393]: https://github.com/bevyengine/bevy/pull/6393 -[6392]: https://github.com/bevyengine/bevy/pull/6392 -[6384]: https://github.com/bevyengine/bevy/pull/6384 -[6382]: https://github.com/bevyengine/bevy/pull/6382 -[6381]: https://github.com/bevyengine/bevy/pull/6381 -[6380]: https://github.com/bevyengine/bevy/pull/6380 -[6379]: https://github.com/bevyengine/bevy/pull/6379 -[6374]: https://github.com/bevyengine/bevy/pull/6374 -[6372]: https://github.com/bevyengine/bevy/pull/6372 -[6360]: https://github.com/bevyengine/bevy/pull/6360 -[6357]: https://github.com/bevyengine/bevy/pull/6357 -[6354]: https://github.com/bevyengine/bevy/pull/6354 -[6351]: https://github.com/bevyengine/bevy/pull/6351 -[6350]: https://github.com/bevyengine/bevy/pull/6350 -[6349]: https://github.com/bevyengine/bevy/pull/6349 -[6345]: https://github.com/bevyengine/bevy/pull/6345 -[6342]: https://github.com/bevyengine/bevy/pull/6342 -[6341]: https://github.com/bevyengine/bevy/pull/6341 -[6337]: https://github.com/bevyengine/bevy/pull/6337 -[6336]: https://github.com/bevyengine/bevy/pull/6336 -[6331]: https://github.com/bevyengine/bevy/pull/6331 -[6329]: https://github.com/bevyengine/bevy/pull/6329 -[6321]: https://github.com/bevyengine/bevy/pull/6321 -[6319]: https://github.com/bevyengine/bevy/pull/6319 -[6318]: https://github.com/bevyengine/bevy/pull/6318 -[6308]: https://github.com/bevyengine/bevy/pull/6308 -[6303]: https://github.com/bevyengine/bevy/pull/6303 -[6290]: https://github.com/bevyengine/bevy/pull/6290 -[6288]: https://github.com/bevyengine/bevy/pull/6288 -[6274]: https://github.com/bevyengine/bevy/pull/6274 -[6273]: https://github.com/bevyengine/bevy/pull/6273 -[6268]: https://github.com/bevyengine/bevy/pull/6268 -[6257]: https://github.com/bevyengine/bevy/pull/6257 -[6249]: https://github.com/bevyengine/bevy/pull/6249 -[6248]: https://github.com/bevyengine/bevy/pull/6248 -[6247]: https://github.com/bevyengine/bevy/pull/6247 -[6242]: https://github.com/bevyengine/bevy/pull/6242 -[6237]: https://github.com/bevyengine/bevy/pull/6237 -[6233]: https://github.com/bevyengine/bevy/pull/6233 -[6232]: https://github.com/bevyengine/bevy/pull/6232 -[6230]: https://github.com/bevyengine/bevy/pull/6230 -[6229]: https://github.com/bevyengine/bevy/pull/6229 -[6227]: https://github.com/bevyengine/bevy/pull/6227 -[6222]: https://github.com/bevyengine/bevy/pull/6222 -[6218]: https://github.com/bevyengine/bevy/pull/6218 -[6214]: https://github.com/bevyengine/bevy/pull/6214 -[6211]: https://github.com/bevyengine/bevy/pull/6211 -[6210]: https://github.com/bevyengine/bevy/pull/6210 -[6209]: https://github.com/bevyengine/bevy/pull/6209 -[6200]: https://github.com/bevyengine/bevy/pull/6200 -[6199]: https://github.com/bevyengine/bevy/pull/6199 -[6194]: https://github.com/bevyengine/bevy/pull/6194 -[6193]: https://github.com/bevyengine/bevy/pull/6193 -[6192]: https://github.com/bevyengine/bevy/pull/6192 -[6191]: https://github.com/bevyengine/bevy/pull/6191 -[6189]: https://github.com/bevyengine/bevy/pull/6189 -[6187]: https://github.com/bevyengine/bevy/pull/6187 -[6185]: https://github.com/bevyengine/bevy/pull/6185 -[6180]: https://github.com/bevyengine/bevy/pull/6180 -[6176]: https://github.com/bevyengine/bevy/pull/6176 -[6170]: https://github.com/bevyengine/bevy/pull/6170 -[6167]: https://github.com/bevyengine/bevy/pull/6167 -[6159]: https://github.com/bevyengine/bevy/pull/6159 -[6158]: https://github.com/bevyengine/bevy/pull/6158 -[6152]: https://github.com/bevyengine/bevy/pull/6152 -[6140]: https://github.com/bevyengine/bevy/pull/6140 -[6134]: https://github.com/bevyengine/bevy/pull/6134 -[6133]: https://github.com/bevyengine/bevy/pull/6133 -[6132]: https://github.com/bevyengine/bevy/pull/6132 -[6127]: https://github.com/bevyengine/bevy/pull/6127 -[6126]: https://github.com/bevyengine/bevy/pull/6126 -[6123]: https://github.com/bevyengine/bevy/pull/6123 -[6121]: https://github.com/bevyengine/bevy/pull/6121 -[6115]: https://github.com/bevyengine/bevy/pull/6115 -[6114]: https://github.com/bevyengine/bevy/pull/6114 -[6113]: https://github.com/bevyengine/bevy/pull/6113 -[6107]: https://github.com/bevyengine/bevy/pull/6107 -[6100]: https://github.com/bevyengine/bevy/pull/6100 -[6088]: https://github.com/bevyengine/bevy/pull/6088 -[6087]: https://github.com/bevyengine/bevy/pull/6087 -[6084]: https://github.com/bevyengine/bevy/pull/6084 -[6083]: https://github.com/bevyengine/bevy/pull/6083 -[6074]: https://github.com/bevyengine/bevy/pull/6074 -[6068]: https://github.com/bevyengine/bevy/pull/6068 -[6066]: https://github.com/bevyengine/bevy/pull/6066 -[6057]: https://github.com/bevyengine/bevy/pull/6057 -[6054]: https://github.com/bevyengine/bevy/pull/6054 -[6049]: https://github.com/bevyengine/bevy/pull/6049 -[6047]: https://github.com/bevyengine/bevy/pull/6047 -[6039]: https://github.com/bevyengine/bevy/pull/6039 -[6029]: https://github.com/bevyengine/bevy/pull/6029 -[6025]: https://github.com/bevyengine/bevy/pull/6025 -[6023]: https://github.com/bevyengine/bevy/pull/6023 -[6020]: https://github.com/bevyengine/bevy/pull/6020 -[6017]: https://github.com/bevyengine/bevy/pull/6017 -[6015]: https://github.com/bevyengine/bevy/pull/6015 -[6014]: https://github.com/bevyengine/bevy/pull/6014 -[6009]: https://github.com/bevyengine/bevy/pull/6009 -[6008]: https://github.com/bevyengine/bevy/pull/6008 -[6000]: https://github.com/bevyengine/bevy/pull/6000 -[5994]: https://github.com/bevyengine/bevy/pull/5994 -[5993]: https://github.com/bevyengine/bevy/pull/5993 -[5991]: https://github.com/bevyengine/bevy/pull/5991 -[5988]: https://github.com/bevyengine/bevy/pull/5988 -[5987]: https://github.com/bevyengine/bevy/pull/5987 -[5982]: https://github.com/bevyengine/bevy/pull/5982 -[5978]: https://github.com/bevyengine/bevy/pull/5978 -[5971]: https://github.com/bevyengine/bevy/pull/5971 -[5968]: https://github.com/bevyengine/bevy/pull/5968 -[5957]: https://github.com/bevyengine/bevy/pull/5957 -[5952]: https://github.com/bevyengine/bevy/pull/5952 -[5949]: https://github.com/bevyengine/bevy/pull/5949 -[5948]: https://github.com/bevyengine/bevy/pull/5948 -[5947]: https://github.com/bevyengine/bevy/pull/5947 -[5945]: https://github.com/bevyengine/bevy/pull/5945 -[5942]: https://github.com/bevyengine/bevy/pull/5942 -[5923]: https://github.com/bevyengine/bevy/pull/5923 -[5917]: https://github.com/bevyengine/bevy/pull/5917 -[5916]: https://github.com/bevyengine/bevy/pull/5916 -[5895]: https://github.com/bevyengine/bevy/pull/5895 -[5886]: https://github.com/bevyengine/bevy/pull/5886 -[5883]: https://github.com/bevyengine/bevy/pull/5883 -[5878]: https://github.com/bevyengine/bevy/pull/5878 -[5877]: https://github.com/bevyengine/bevy/pull/5877 -[5871]: https://github.com/bevyengine/bevy/pull/5871 -[5865]: https://github.com/bevyengine/bevy/pull/5865 -[5864]: https://github.com/bevyengine/bevy/pull/5864 -[5863]: https://github.com/bevyengine/bevy/pull/5863 -[5857]: https://github.com/bevyengine/bevy/pull/5857 -[5855]: https://github.com/bevyengine/bevy/pull/5855 -[5854]: https://github.com/bevyengine/bevy/pull/5854 -[5853]: https://github.com/bevyengine/bevy/pull/5853 -[5847]: https://github.com/bevyengine/bevy/pull/5847 -[5838]: https://github.com/bevyengine/bevy/pull/5838 -[5835]: https://github.com/bevyengine/bevy/pull/5835 -[5827]: https://github.com/bevyengine/bevy/pull/5827 -[5826]: https://github.com/bevyengine/bevy/pull/5826 -[5825]: https://github.com/bevyengine/bevy/pull/5825 -[5819]: https://github.com/bevyengine/bevy/pull/5819 -[5814]: https://github.com/bevyengine/bevy/pull/5814 -[5813]: https://github.com/bevyengine/bevy/pull/5813 -[5806]: https://github.com/bevyengine/bevy/pull/5806 -[5797]: https://github.com/bevyengine/bevy/pull/5797 -[5783]: https://github.com/bevyengine/bevy/pull/5783 -[5782]: https://github.com/bevyengine/bevy/pull/5782 -[5780]: https://github.com/bevyengine/bevy/pull/5780 -[5776]: https://github.com/bevyengine/bevy/pull/5776 -[5766]: https://github.com/bevyengine/bevy/pull/5766 -[5761]: https://github.com/bevyengine/bevy/pull/5761 -[5758]: https://github.com/bevyengine/bevy/pull/5758 -[5757]: https://github.com/bevyengine/bevy/pull/5757 -[5752]: https://github.com/bevyengine/bevy/pull/5752 -[5751]: https://github.com/bevyengine/bevy/pull/5751 -[5747]: https://github.com/bevyengine/bevy/pull/5747 -[5731]: https://github.com/bevyengine/bevy/pull/5731 -[5730]: https://github.com/bevyengine/bevy/pull/5730 -[5729]: https://github.com/bevyengine/bevy/pull/5729 -[5728]: https://github.com/bevyengine/bevy/pull/5728 -[5723]: https://github.com/bevyengine/bevy/pull/5723 -[5720]: https://github.com/bevyengine/bevy/pull/5720 -[5708]: https://github.com/bevyengine/bevy/pull/5708 -[5688]: https://github.com/bevyengine/bevy/pull/5688 -[5686]: https://github.com/bevyengine/bevy/pull/5686 -[5685]: https://github.com/bevyengine/bevy/pull/5685 -[5676]: https://github.com/bevyengine/bevy/pull/5676 -[5666]: https://github.com/bevyengine/bevy/pull/5666 -[5664]: https://github.com/bevyengine/bevy/pull/5664 -[5658]: https://github.com/bevyengine/bevy/pull/5658 -[5635]: https://github.com/bevyengine/bevy/pull/5635 -[5630]: https://github.com/bevyengine/bevy/pull/5630 -[5626]: https://github.com/bevyengine/bevy/pull/5626 -[5613]: https://github.com/bevyengine/bevy/pull/5613 -[5608]: https://github.com/bevyengine/bevy/pull/5608 -[5601]: https://github.com/bevyengine/bevy/pull/5601 -[5593]: https://github.com/bevyengine/bevy/pull/5593 -[5587]: https://github.com/bevyengine/bevy/pull/5587 -[5586]: https://github.com/bevyengine/bevy/pull/5586 -[5582]: https://github.com/bevyengine/bevy/pull/5582 -[5577]: https://github.com/bevyengine/bevy/pull/5577 -[5566]: https://github.com/bevyengine/bevy/pull/5566 -[5560]: https://github.com/bevyengine/bevy/pull/5560 -[5558]: https://github.com/bevyengine/bevy/pull/5558 -[5557]: https://github.com/bevyengine/bevy/pull/5557 -[5556]: https://github.com/bevyengine/bevy/pull/5556 -[5554]: https://github.com/bevyengine/bevy/pull/5554 -[5546]: https://github.com/bevyengine/bevy/pull/5546 -[5537]: https://github.com/bevyengine/bevy/pull/5537 -[5531]: https://github.com/bevyengine/bevy/pull/5531 -[5527]: https://github.com/bevyengine/bevy/pull/5527 -[5512]: https://github.com/bevyengine/bevy/pull/5512 -[5509]: https://github.com/bevyengine/bevy/pull/5509 -[5495]: https://github.com/bevyengine/bevy/pull/5495 -[5483]: https://github.com/bevyengine/bevy/pull/5483 -[5481]: https://github.com/bevyengine/bevy/pull/5481 -[5473]: https://github.com/bevyengine/bevy/pull/5473 -[5461]: https://github.com/bevyengine/bevy/pull/5461 -[5413]: https://github.com/bevyengine/bevy/pull/5413 -[5409]: https://github.com/bevyengine/bevy/pull/5409 -[5402]: https://github.com/bevyengine/bevy/pull/5402 -[5340]: https://github.com/bevyengine/bevy/pull/5340 -[5325]: https://github.com/bevyengine/bevy/pull/5325 -[5321]: https://github.com/bevyengine/bevy/pull/5321 -[5296]: https://github.com/bevyengine/bevy/pull/5296 -[5264]: https://github.com/bevyengine/bevy/pull/5264 -[5250]: https://github.com/bevyengine/bevy/pull/5250 -[5205]: https://github.com/bevyengine/bevy/pull/5205 -[5096]: https://github.com/bevyengine/bevy/pull/5096 -[4992]: https://github.com/bevyengine/bevy/pull/4992 -[4970]: https://github.com/bevyengine/bevy/pull/4970 -[4919]: https://github.com/bevyengine/bevy/pull/4919 -[4809]: https://github.com/bevyengine/bevy/pull/4809 -[4800]: https://github.com/bevyengine/bevy/pull/4800 -[4761]: https://github.com/bevyengine/bevy/pull/4761 -[4724]: https://github.com/bevyengine/bevy/pull/4724 -[4466]: https://github.com/bevyengine/bevy/pull/4466 -[4460]: https://github.com/bevyengine/bevy/pull/4460 -[4452]: https://github.com/bevyengine/bevy/pull/4452 -[4352]: https://github.com/bevyengine/bevy/pull/4352 -[4109]: https://github.com/bevyengine/bevy/pull/4109 -[3425]: https://github.com/bevyengine/bevy/pull/3425 -[3009]: https://github.com/bevyengine/bevy/pull/3009 -[2975]: https://github.com/bevyengine/bevy/pull/2975 - -## Version 0.8.0 (2022-07-30) - -### Added - -- [Callable PBR functions][4939] -- [Spotlights][4715] -- [Camera Driven Rendering][4745] -- [Camera Driven Viewports][4898] -- [Visibilty Inheritance, universal `ComputedVisibility`, and `RenderLayers` support][5310] -- [Better Materials: `AsBindGroup` trait and derive, simpler `Material` trait][5053] -- [Derive `AsBindGroup` Improvements: Better errors, more options, update examples][5364] -- [Support `AsBindGroup` for 2d materials as well][5312] -- [Parallel Frustum Culling][4489] -- [Hierarchy commandization][4197] -- [Generate vertex tangents using mikktspace][3872] -- [Add a `SpatialBundle` with `Visibility` and `Transform` components][5344] -- [Add `RegularPolygon` and `Circle` meshes][3730] -- [Add a `SceneBundle` to spawn a scene][2424] -- [Allow higher order systems][4833] -- [Add global `init()` and `get()` accessors for all newtyped `TaskPools`][2250] -- [Add reusable shader functions for transforming position/normal/tangent][4901] -- [Add support for vertex colors][4528] -- [Add support for removing attributes from meshes][5254] -- [Add option to center a window][4999] -- [Add `depth_load_op` configuration field to `Camera3d`][4904] -- [Refactor `Camera` methods and add viewport rect][4948] -- [Add `TextureFormat::R16Unorm` support for `Image`][5249] -- [Add a `VisibilityBundle` with `Visibility` and `ComputedVisibility` components][5335] -- [Add ExtractResourcePlugin][3745] -- [Add depth_bias to SpecializedMaterial][4101] -- [Added `offset` parameter to `TextureAtlas::from_grid_with_padding`][4836] -- [Add the possibility to create custom 2d orthographic cameras][4048] -- [bevy_render: Add `attributes` and `attributes_mut` methods to `Mesh`][3927] -- [Add helper methods for rotating `Transform`s][5151] -- [Enable wgpu profiling spans when using bevy's trace feature][5182] -- [bevy_pbr: rework `extract_meshes`][4240] -- [Add `inverse_projection` and `inverse_view_proj` fields to shader view uniform][5119] -- [Add `ViewRangefinder3d` to reduce boilerplate when enqueuing standard 3D `PhaseItems`][5014] -- [Create `bevy_ptr` standalone crate][4653] -- [Add `IntoIterator` impls for `&Query` and `&mut Query`][4692] -- [Add untyped APIs for `Components` and `Resources`][4447] -- [Add infallible resource getters for `WorldCell`][4104] -- [Add `get_change_ticks` method to `EntityRef` and `EntityMut`][2539] -- [Add comparison methods to `FilteredAccessSet`][4211] -- [Add `Commands::new_from_entities`][4423] -- [Add `QueryState::get_single_unchecked_manual` and its family members][4841] -- [Add `ParallelCommands` system parameter][4749] -- [Add methods for querying lists of entities][4879] -- [Implement `FusedIterator` for eligible `Iterator` types][4942] -- [Add `component_id()` function to `World` and `Components`][5066] -- [Add ability to inspect entity's components][5136] -- [Add a more helpful error to help debug panicking command on despawned entity][5198] -- [Add `ExactSizeIterator` implementation for `QueryCombinatonIter`][5148] -- [Added the `ignore_fields` attribute to the derive macros for `*Label` types][5366] -- [Exact sized event iterators][3863] -- [Add a `clear()` method to the `EventReader` that consumes the iterator][4693] -- [Add helpers to send `Events` from `World`][5355] -- [Add file metadata to `AssetIo`][2123] -- [Add missing audio/ogg file extensions: .oga, .spx][4703] -- [Add `reload_asset` method to `AssetServer`][5106] -- [Allow specifying chrome tracing file path using an environment variable][4618] -- [Create a simple tool to compare traces between executions][4628] -- [Add a tracing span for run criteria][4709] -- [Add tracing spans for `Query::par_for_each` and its variants.][4711] -- [Add a `release_all` method on `Input`][5011] -- [Add a `reset_all` method on `Input`][5015] -- [Add a helper tool to build examples for wasm][4776] -- [bevy_reflect: add a `ReflectFromPtr` type to create `&dyn Reflect` from a `*const ()`][4475] -- [Add a `ReflectDefault` type and add `#[reflect(Default)]` to all component types that implement Default and are user facing][3733] -- [Add a macro to implement `Reflect` for struct types and migrate glam types to use this for reflection][4540] -- [bevy_reflect: reflect arrays][4701] -- [bevy_reflect: reflect char][4790] -- [bevy_reflect: add `GetTypeRegistration` impl for reflected tuples][4226] -- [Add reflection for `Resources`][5175] -- [bevy_reflect: add `as_reflect` and `as_reflect_mut` methods on `Reflect`][4350] -- [Add an `apply_or_insert` method to `ReflectResource` and `ReflectComponent`][5201] -- [bevy_reflect: `IntoIter` for `DynamicList` and `DynamicMap`][4108] -- [bevy_reflect: Add `PartialEq` to reflected `f32`s and `f64`s][4217] -- [Create mutable versions of `TypeRegistry` methods][4484] -- [bevy_reflect: add a `get_boxed` method to `reflect_trait`][4120] -- [bevy_reflect: add `#[reflect(default)]` attribute for `FromReflect`][4140] -- [bevy_reflect: add statically available type info for reflected types][4042] -- [Add an `assert_is_exclusive_system` function][5275] -- [bevy_ui: add a multi-windows check for `Interaction` (we dont yet support multiple windows)][5225] - -### Changed - -- [Depend on Taffy (a Dioxus and Bevy-maintained fork of Stretch)][4716] -- [Use lifetimed, type erased pointers in bevy_ecs][3001] -- [Migrate to `encase` from `crevice`][4339] -- [Update `wgpu` to 0.13][5168] -- [Pointerfication followup: Type safety and cleanup][4621] -- [bevy_ptr works in no_std environments][4760] -- [Fail to compile on 16-bit platforms][4736] -- [Improve ergonomics and reduce boilerplate around creating text elements][5343] -- [Don't cull `Ui` nodes that have a rotation][5389] -- [Rename `ElementState` to `ButtonState`][4314] -- [Move `Size` to `bevy_ui`][4285] -- [Move `Rect` to `bevy_ui` and rename it to `UiRect`][4276] -- [Modify `FontAtlas` so that it can handle fonts of any size][3592] -- [Rename `CameraUi`][5234] -- [Remove `task_pool` parameter from `par_for_each(_mut)`][4705] -- [Copy `TaskPool` resoures to sub-Apps][4792] -- [Allow closing windows at runtime][3575] -- [Move the configuration of the `WindowPlugin` to a `Resource`][5227] -- [Optionally resize `Window` canvas element to fit parent element][4726] -- [Change window resolution types from tuple to `Vec2`][5276] -- [Update time by sending frame `Instant` through a channel][4744] -- [Split time functionality into `bevy_time`][4187] -- [Split mesh shader files to make the shaders more reusable][4867] -- [Set `naga` capabilities corresponding to `wgpu` features][4824] -- [Separate out PBR lighting, shadows, clustered forward, and utils from pbr.wgsl][4938] -- [Separate PBR and tonemapping into 2 functions][5078] -- [Make `RenderStage::Extract` run on the render world][4402] -- [Change default `FilterMode` of `Image` to `Linear`][4465] -- [bevy_render: Fix KTX2 UASTC format mapping][4569] -- [Allow rendering meshes without UV coordinate data][5222] -- [Validate vertex attribute format on insertion][5259] -- [Use `Affine3A` for `GlobalTransform`to allow any affine transformation][4379] -- [Recalculate entity `AABB`s when meshes change][4944] -- [Change `check_visibility` to use thread-local queues instead of a channel][4663] -- [Allow unbatched render phases to use unstable sorts][5049] -- [Extract resources into their target location][5271] -- [Enable loading textures of unlimited size][5305] -- [Do not create nor execute render passes which have no `PhaseItems` to draw][4643] -- [Filter material handles on extraction][4178] -- [Apply vertex colors to `ColorMaterial` and `Mesh2D`][4812] -- [Make `MaterialPipelineKey` fields public][4508] -- [Simplified API to get NDC from camera and world position][4041] -- [Set `alpha_mode` based on alpha value][4658] -- [Make `Wireframe` respect `VisibleEntities`][4660] -- [Use const `Vec2` in lights cluster and bounding box when possible][4602] -- [Make accessors for mesh vertices and indices public][3906] -- [Use `BufferUsages::UNIFORM` for `SkinnedMeshUniform`][4816] -- [Place origin of `OrthographicProjection` at integer pixel when using `ScalingMode::WindowSize`][4085] -- [Make `ScalingMode` more flexible][3253] -- [Move texture sample out of branch in `prepare_normal`][5129] -- [Make the fields of the `Material2dKey` public][5212] -- [Use collect to build mesh attributes][5255] -- [Replace `ReadOnlyFetch` with `ReadOnlyWorldQuery`][4626] -- [Replace `ComponentSparseSet`'s internals with a `Column`][4909] -- [Remove QF generics from all `Query/State` methods and types][5170] -- [Remove `.system()`][4499] -- [Make change lifespan deterministic and update docs][3956] -- [Make derived `SystemParam` readonly if possible][4650] -- [Merge `matches_archetype` and `matches_table`][4807] -- [Allows conversion of mutable queries to immutable queries][5376] -- [Skip `drop` when `needs_drop` is `false`][4773] -- [Use u32 over usize for `ComponentSparseSet` indicies][4723] -- [Remove redundant `ComponentId` in `Column`][4855] -- [Directly copy moved `Table` components to the target location][5056] -- [`SystemSet::before` and `SystemSet::after` now take `AsSystemLabel`][4503] -- [Converted exclusive systems to parallel systems wherever possible][2774] -- [Improve `size_hint` on `QueryIter`][4244] -- [Improve debugging tools for change detection][4160] -- [Make `RunOnce` a non-manual `System` impl][3922] -- [Apply buffers in `ParamSet`][4677] -- [Don't allocate for `ComponentDescriptors` of non-dynamic component types][4725] -- [Mark mutable APIs under ECS storage as `pub(crate)`][5065] -- [Update `ExactSizeIterator` impl to support archetypal filters (`With`, `Without`)][5124] -- [Removed world cell from places where split multable access is not needed][5167] -- [Add Events to `bevy_ecs` prelude][5159] -- [Improve `EntityMap` API][5231] -- [Implement `From` for `ShouldRun`.][5306] -- [Allow iter combinations on custom world queries][5286] -- [Simplify design for `*Label`s][4957] -- [Tidy up the code of events][4713] -- [Rename `send_default_event` to `send_event_default` on world][5383] -- [enable optional dependencies to stay optional][5023] -- [Remove the dependency cycles][5171] -- [Enforce type safe usage of Handle::get][4794] -- [Export anyhow::error for custom asset loaders][5359] -- [Update `shader_material_glsl` example to include texture sampling][5215] -- [Remove unused code in game of life shader][5349] -- [Make the contributor birbs bounce to the window height][5274] -- [Improve Gamepad D-Pad Button Detection][5220] -- [bevy_reflect: support map insertio][5173] -- [bevy_reflect: improve debug formatting for reflected types][4218] -- [bevy_reflect_derive: big refactor tidying up the code][4712] -- [bevy_reflect: small refactor and default `Reflect` methods][4739] -- [Make `Reflect` safe to implement][5010] -- [`bevy_reflect`: put `serialize` into external `ReflectSerialize` type][4782] -- [Remove `Serialize` impl for `dyn Array` and friends][4780] -- [Re-enable `#[derive(TypeUuid)]` for generics][4118] -- [Move primitive type registration into `bevy_reflect`][4844] -- [Implement reflection for more `glam` types][5194] -- [Make `reflect_partial_eq` return more accurate results][5210] -- [Make public macros more robust with `$crate`][4655] -- [Ensure that the parent is always the expected entity][4717] -- [Support returning data out of `with_children`][4708] -- [Remove `EntityMut::get_unchecked`][4547] -- [Diagnostics: meaningful error when graph node has wrong number of inputs][4924] -- [Remove redundant `Size` import][5339] -- [Export and register `Mat2`.][5324] -- [Implement `Debug` for `Gamepads`][5291] -- [Update codebase to use `IntoIterator` where possible.][5269] -- [Rename `headless_defaults` example to `no_renderer` for clarity][5263] -- [Remove dead `SystemLabelMarker` struct][5190] -- [bevy_reflect: remove `glam` from a test which is active without the glam feature][5195] -- [Disable vsync for stress tests][5187] -- [Move `get_short_name` utility method from `bevy_reflect` into `bevy_utils`][5174] -- [Derive `Default` for enums where possible][5158] -- [Implement `Eq` and `PartialEq` for `MouseScrollUnit`][5048] -- [Some cleanup for `bevy_ptr`][4668] -- [Move float_ord from `bevy_core` to `bevy_utils`][4189] -- [Remove unused `CountdownEvent`][4290] -- [Some minor cleanups of asset_server][4604] -- [Use `elapsed()` on `Instant`][4599] -- [Make paused `Timers` update `just_finished` on tick][4445] -- [bevy_utils: remove hardcoded log level limit][4580] -- [Make `Time::update_with_instant` public for use in tests][4469] -- [Do not impl Component for Task][4113] -- [Remove nonexistent `WgpuResourceDiagnosticsPlugin`][4541] -- [Update ndk-glue requirement from 0.5 to 0.6][3624] -- [Update tracing-tracy requirement from 0.8.0 to 0.9.0][4786] -- [update image to 0.24][4121] -- [update xshell to 0.2][4789] -- [Update gilrs to v0.9][4848] -- [bevy_log: upgrade to tracing-tracy 0.10.0][4991] -- [update hashbrown to 0.12][5035] -- [Update `clap` to 3.2 in tools using `value_parser`][5031] -- [Updated `glam` to `0.21`.][5142] -- [Update Notify Dependency][5396] - -### Fixed - -- [bevy_ui: keep `Color` as 4 `f32`s][4494] -- [Fix issues with bevy on android other than the rendering][5130] -- [Update layout/style when scale factor changes too][4689] -- [Fix `Overflow::Hidden` so it works correctly with `scale_factor_override`][3854] -- [Fix `bevy_ui` touch input][4099] -- [Fix physical viewport calculation][5055] -- [Minimally fix the known unsoundness in `bevy_mikktspace`][5299] -- [Make `Transform` propagation correct in the presence of updated children][4608] -- [`StorageBuffer` uses wrong type to calculate the buffer size.][4557] -- [Fix confusing near and far fields in Camera][4457] -- [Allow minimising window if using a 2d camera][4527] -- [WGSL: use correct syntax for matrix access][5039] -- [Gltf: do not import `IoTaskPool` in wasm][5038] -- [Fix skinned mesh normal handling in mesh shader][5095] -- [Don't panic when `StandardMaterial` `normal_map` hasn't loaded yet][5307] -- [Fix incorrect rotation in `Transform::rotate_around`][5300] -- [Fix `extract_wireframes`][5301] -- [Fix type parameter name conflicts of `#[derive(Bundle)]`][4636] -- [Remove unnecessary `unsafe impl` of `Send+Sync` for `ParallelSystemContainer`][5137] -- [Fix line material shader][5348] -- [Fix `mouse_clicked` check for touch][2029] -- [Fix unsoundness with `Or`/`AnyOf`/`Option` component access][4659] -- [Improve soundness of `CommandQueue`][4863] -- [Fix some memory leaks detected by miri][4959] -- [Fix Android example icon][4076] -- [Fix broken `WorldCell` test][5009] -- [Bugfix `State::set` transition condition infinite loop][4890] -- [Fix crash when using `Duration::MAX`][4900] -- [Fix release builds: Move asserts under `#[cfg(debug_assertions)]`][4871] -- [Fix frame count being a float][4493] -- [Fix "unused" warnings when compiling with `render` feature but without `animation`][4714] -- [Fix re-adding a plugin to a `PluginGroup`][2039] -- [Fix torus normals][4520] -- [Add `NO_STORAGE_BUFFERS_SUPPORT` shaderdef when needed][4949] - -[2029]: https://github.com/bevyengine/bevy/pull/2029 -[2039]: https://github.com/bevyengine/bevy/pull/2039 -[2123]: https://github.com/bevyengine/bevy/pull/2123 -[2250]: https://github.com/bevyengine/bevy/pull/2250 -[2424]: https://github.com/bevyengine/bevy/pull/2424 -[2539]: https://github.com/bevyengine/bevy/pull/2539 -[2774]: https://github.com/bevyengine/bevy/pull/2774 -[3001]: https://github.com/bevyengine/bevy/pull/3001 -[3253]: https://github.com/bevyengine/bevy/pull/3253 -[3575]: https://github.com/bevyengine/bevy/pull/3575 -[3592]: https://github.com/bevyengine/bevy/pull/3592 -[3624]: https://github.com/bevyengine/bevy/pull/3624 -[3730]: https://github.com/bevyengine/bevy/pull/3730 -[3733]: https://github.com/bevyengine/bevy/pull/3733 -[3745]: https://github.com/bevyengine/bevy/pull/3745 -[3854]: https://github.com/bevyengine/bevy/pull/3854 -[3863]: https://github.com/bevyengine/bevy/pull/3863 -[3872]: https://github.com/bevyengine/bevy/pull/3872 -[3906]: https://github.com/bevyengine/bevy/pull/3906 -[3922]: https://github.com/bevyengine/bevy/pull/3922 -[3927]: https://github.com/bevyengine/bevy/pull/3927 -[3956]: https://github.com/bevyengine/bevy/pull/3956 -[4041]: https://github.com/bevyengine/bevy/pull/4041 -[4042]: https://github.com/bevyengine/bevy/pull/4042 -[4048]: https://github.com/bevyengine/bevy/pull/4048 -[4076]: https://github.com/bevyengine/bevy/pull/4076 -[4085]: https://github.com/bevyengine/bevy/pull/4085 -[4099]: https://github.com/bevyengine/bevy/pull/4099 -[4101]: https://github.com/bevyengine/bevy/pull/4101 -[4104]: https://github.com/bevyengine/bevy/pull/4104 -[4108]: https://github.com/bevyengine/bevy/pull/4108 -[4113]: https://github.com/bevyengine/bevy/pull/4113 -[4118]: https://github.com/bevyengine/bevy/pull/4118 -[4120]: https://github.com/bevyengine/bevy/pull/4120 -[4121]: https://github.com/bevyengine/bevy/pull/4121 -[4140]: https://github.com/bevyengine/bevy/pull/4140 -[4160]: https://github.com/bevyengine/bevy/pull/4160 -[4178]: https://github.com/bevyengine/bevy/pull/4178 -[4187]: https://github.com/bevyengine/bevy/pull/4187 -[4189]: https://github.com/bevyengine/bevy/pull/4189 -[4197]: https://github.com/bevyengine/bevy/pull/4197 -[4211]: https://github.com/bevyengine/bevy/pull/4211 -[4217]: https://github.com/bevyengine/bevy/pull/4217 -[4218]: https://github.com/bevyengine/bevy/pull/4218 -[4226]: https://github.com/bevyengine/bevy/pull/4226 -[4240]: https://github.com/bevyengine/bevy/pull/4240 -[4244]: https://github.com/bevyengine/bevy/pull/4244 -[4276]: https://github.com/bevyengine/bevy/pull/4276 -[4285]: https://github.com/bevyengine/bevy/pull/4285 -[4290]: https://github.com/bevyengine/bevy/pull/4290 -[4314]: https://github.com/bevyengine/bevy/pull/4314 -[4339]: https://github.com/bevyengine/bevy/pull/4339 -[4350]: https://github.com/bevyengine/bevy/pull/4350 -[4379]: https://github.com/bevyengine/bevy/pull/4379 -[4402]: https://github.com/bevyengine/bevy/pull/4402 -[4423]: https://github.com/bevyengine/bevy/pull/4423 -[4445]: https://github.com/bevyengine/bevy/pull/4445 -[4447]: https://github.com/bevyengine/bevy/pull/4447 -[4457]: https://github.com/bevyengine/bevy/pull/4457 -[4465]: https://github.com/bevyengine/bevy/pull/4465 -[4469]: https://github.com/bevyengine/bevy/pull/4469 -[4475]: https://github.com/bevyengine/bevy/pull/4475 -[4484]: https://github.com/bevyengine/bevy/pull/4484 -[4489]: https://github.com/bevyengine/bevy/pull/4489 -[4493]: https://github.com/bevyengine/bevy/pull/4493 -[4494]: https://github.com/bevyengine/bevy/pull/4494 -[4499]: https://github.com/bevyengine/bevy/pull/4499 -[4503]: https://github.com/bevyengine/bevy/pull/4503 -[4508]: https://github.com/bevyengine/bevy/pull/4508 -[4520]: https://github.com/bevyengine/bevy/pull/4520 -[4527]: https://github.com/bevyengine/bevy/pull/4527 -[4528]: https://github.com/bevyengine/bevy/pull/4528 -[4540]: https://github.com/bevyengine/bevy/pull/4540 -[4541]: https://github.com/bevyengine/bevy/pull/4541 -[4547]: https://github.com/bevyengine/bevy/pull/4547 -[4557]: https://github.com/bevyengine/bevy/pull/4557 -[4569]: https://github.com/bevyengine/bevy/pull/4569 -[4580]: https://github.com/bevyengine/bevy/pull/4580 -[4599]: https://github.com/bevyengine/bevy/pull/4599 -[4602]: https://github.com/bevyengine/bevy/pull/4602 -[4604]: https://github.com/bevyengine/bevy/pull/4604 -[4608]: https://github.com/bevyengine/bevy/pull/4608 -[4618]: https://github.com/bevyengine/bevy/pull/4618 -[4621]: https://github.com/bevyengine/bevy/pull/4621 -[4626]: https://github.com/bevyengine/bevy/pull/4626 -[4628]: https://github.com/bevyengine/bevy/pull/4628 -[4636]: https://github.com/bevyengine/bevy/pull/4636 -[4643]: https://github.com/bevyengine/bevy/pull/4643 -[4650]: https://github.com/bevyengine/bevy/pull/4650 -[4653]: https://github.com/bevyengine/bevy/pull/4653 -[4655]: https://github.com/bevyengine/bevy/pull/4655 -[4658]: https://github.com/bevyengine/bevy/pull/4658 -[4659]: https://github.com/bevyengine/bevy/pull/4659 -[4660]: https://github.com/bevyengine/bevy/pull/4660 -[4663]: https://github.com/bevyengine/bevy/pull/4663 -[4668]: https://github.com/bevyengine/bevy/pull/4668 -[4677]: https://github.com/bevyengine/bevy/pull/4677 -[4689]: https://github.com/bevyengine/bevy/pull/4689 -[4692]: https://github.com/bevyengine/bevy/pull/4692 -[4693]: https://github.com/bevyengine/bevy/pull/4693 -[4701]: https://github.com/bevyengine/bevy/pull/4701 -[4703]: https://github.com/bevyengine/bevy/pull/4703 -[4705]: https://github.com/bevyengine/bevy/pull/4705 -[4708]: https://github.com/bevyengine/bevy/pull/4708 -[4709]: https://github.com/bevyengine/bevy/pull/4709 -[4711]: https://github.com/bevyengine/bevy/pull/4711 -[4712]: https://github.com/bevyengine/bevy/pull/4712 -[4713]: https://github.com/bevyengine/bevy/pull/4713 -[4714]: https://github.com/bevyengine/bevy/pull/4714 -[4715]: https://github.com/bevyengine/bevy/pull/4715 -[4716]: https://github.com/bevyengine/bevy/pull/4716 -[4717]: https://github.com/bevyengine/bevy/pull/4717 -[4723]: https://github.com/bevyengine/bevy/pull/4723 -[4725]: https://github.com/bevyengine/bevy/pull/4725 -[4726]: https://github.com/bevyengine/bevy/pull/4726 -[4736]: https://github.com/bevyengine/bevy/pull/4736 -[4739]: https://github.com/bevyengine/bevy/pull/4739 -[4744]: https://github.com/bevyengine/bevy/pull/4744 -[4745]: https://github.com/bevyengine/bevy/pull/4745 -[4749]: https://github.com/bevyengine/bevy/pull/4749 -[4760]: https://github.com/bevyengine/bevy/pull/4760 -[4773]: https://github.com/bevyengine/bevy/pull/4773 -[4776]: https://github.com/bevyengine/bevy/pull/4776 -[4780]: https://github.com/bevyengine/bevy/pull/4780 -[4782]: https://github.com/bevyengine/bevy/pull/4782 -[4786]: https://github.com/bevyengine/bevy/pull/4786 -[4789]: https://github.com/bevyengine/bevy/pull/4789 -[4790]: https://github.com/bevyengine/bevy/pull/4790 -[4792]: https://github.com/bevyengine/bevy/pull/4792 -[4794]: https://github.com/bevyengine/bevy/pull/4794 -[4807]: https://github.com/bevyengine/bevy/pull/4807 -[4812]: https://github.com/bevyengine/bevy/pull/4812 -[4816]: https://github.com/bevyengine/bevy/pull/4816 -[4824]: https://github.com/bevyengine/bevy/pull/4824 -[4833]: https://github.com/bevyengine/bevy/pull/4833 -[4836]: https://github.com/bevyengine/bevy/pull/4836 -[4841]: https://github.com/bevyengine/bevy/pull/4841 -[4844]: https://github.com/bevyengine/bevy/pull/4844 -[4848]: https://github.com/bevyengine/bevy/pull/4848 -[4855]: https://github.com/bevyengine/bevy/pull/4855 -[4863]: https://github.com/bevyengine/bevy/pull/4863 -[4867]: https://github.com/bevyengine/bevy/pull/4867 -[4871]: https://github.com/bevyengine/bevy/pull/4871 -[4879]: https://github.com/bevyengine/bevy/pull/4879 -[4890]: https://github.com/bevyengine/bevy/pull/4890 -[4898]: https://github.com/bevyengine/bevy/pull/4898 -[4900]: https://github.com/bevyengine/bevy/pull/4900 -[4901]: https://github.com/bevyengine/bevy/pull/4901 -[4904]: https://github.com/bevyengine/bevy/pull/4904 -[4909]: https://github.com/bevyengine/bevy/pull/4909 -[4924]: https://github.com/bevyengine/bevy/pull/4924 -[4938]: https://github.com/bevyengine/bevy/pull/4938 -[4939]: https://github.com/bevyengine/bevy/pull/4939 -[4942]: https://github.com/bevyengine/bevy/pull/4942 -[4944]: https://github.com/bevyengine/bevy/pull/4944 -[4948]: https://github.com/bevyengine/bevy/pull/4948 -[4949]: https://github.com/bevyengine/bevy/pull/4949 -[4957]: https://github.com/bevyengine/bevy/pull/4957 -[4959]: https://github.com/bevyengine/bevy/pull/4959 -[4991]: https://github.com/bevyengine/bevy/pull/4991 -[4999]: https://github.com/bevyengine/bevy/pull/4999 -[5009]: https://github.com/bevyengine/bevy/pull/5009 -[5010]: https://github.com/bevyengine/bevy/pull/5010 -[5011]: https://github.com/bevyengine/bevy/pull/5011 -[5014]: https://github.com/bevyengine/bevy/pull/5014 -[5015]: https://github.com/bevyengine/bevy/pull/5015 -[5023]: https://github.com/bevyengine/bevy/pull/5023 -[5031]: https://github.com/bevyengine/bevy/pull/5031 -[5035]: https://github.com/bevyengine/bevy/pull/5035 -[5038]: https://github.com/bevyengine/bevy/pull/5038 -[5039]: https://github.com/bevyengine/bevy/pull/5039 -[5048]: https://github.com/bevyengine/bevy/pull/5048 -[5049]: https://github.com/bevyengine/bevy/pull/5049 -[5053]: https://github.com/bevyengine/bevy/pull/5053 -[5055]: https://github.com/bevyengine/bevy/pull/5055 -[5056]: https://github.com/bevyengine/bevy/pull/5056 -[5065]: https://github.com/bevyengine/bevy/pull/5065 -[5066]: https://github.com/bevyengine/bevy/pull/5066 -[5078]: https://github.com/bevyengine/bevy/pull/5078 -[5095]: https://github.com/bevyengine/bevy/pull/5095 -[5106]: https://github.com/bevyengine/bevy/pull/5106 -[5119]: https://github.com/bevyengine/bevy/pull/5119 -[5124]: https://github.com/bevyengine/bevy/pull/5124 -[5129]: https://github.com/bevyengine/bevy/pull/5129 -[5130]: https://github.com/bevyengine/bevy/pull/5130 -[5136]: https://github.com/bevyengine/bevy/pull/5136 -[5137]: https://github.com/bevyengine/bevy/pull/5137 -[5142]: https://github.com/bevyengine/bevy/pull/5142 -[5148]: https://github.com/bevyengine/bevy/pull/5148 -[5151]: https://github.com/bevyengine/bevy/pull/5151 -[5158]: https://github.com/bevyengine/bevy/pull/5158 -[5159]: https://github.com/bevyengine/bevy/pull/5159 -[5167]: https://github.com/bevyengine/bevy/pull/5167 -[5168]: https://github.com/bevyengine/bevy/pull/5168 -[5170]: https://github.com/bevyengine/bevy/pull/5170 -[5171]: https://github.com/bevyengine/bevy/pull/5171 -[5173]: https://github.com/bevyengine/bevy/pull/5173 -[5174]: https://github.com/bevyengine/bevy/pull/5174 -[5175]: https://github.com/bevyengine/bevy/pull/5175 -[5182]: https://github.com/bevyengine/bevy/pull/5182 -[5187]: https://github.com/bevyengine/bevy/pull/5187 -[5190]: https://github.com/bevyengine/bevy/pull/5190 -[5194]: https://github.com/bevyengine/bevy/pull/5194 -[5195]: https://github.com/bevyengine/bevy/pull/5195 -[5198]: https://github.com/bevyengine/bevy/pull/5198 -[5201]: https://github.com/bevyengine/bevy/pull/5201 -[5210]: https://github.com/bevyengine/bevy/pull/5210 -[5212]: https://github.com/bevyengine/bevy/pull/5212 -[5215]: https://github.com/bevyengine/bevy/pull/5215 -[5220]: https://github.com/bevyengine/bevy/pull/5220 -[5222]: https://github.com/bevyengine/bevy/pull/5222 -[5225]: https://github.com/bevyengine/bevy/pull/5225 -[5227]: https://github.com/bevyengine/bevy/pull/5227 -[5231]: https://github.com/bevyengine/bevy/pull/5231 -[5234]: https://github.com/bevyengine/bevy/pull/5234 -[5249]: https://github.com/bevyengine/bevy/pull/5249 -[5254]: https://github.com/bevyengine/bevy/pull/5254 -[5255]: https://github.com/bevyengine/bevy/pull/5255 -[5259]: https://github.com/bevyengine/bevy/pull/5259 -[5263]: https://github.com/bevyengine/bevy/pull/5263 -[5269]: https://github.com/bevyengine/bevy/pull/5269 -[5271]: https://github.com/bevyengine/bevy/pull/5271 -[5274]: https://github.com/bevyengine/bevy/pull/5274 -[5275]: https://github.com/bevyengine/bevy/pull/5275 -[5276]: https://github.com/bevyengine/bevy/pull/5276 -[5286]: https://github.com/bevyengine/bevy/pull/5286 -[5291]: https://github.com/bevyengine/bevy/pull/5291 -[5299]: https://github.com/bevyengine/bevy/pull/5299 -[5300]: https://github.com/bevyengine/bevy/pull/5300 -[5301]: https://github.com/bevyengine/bevy/pull/5301 -[5305]: https://github.com/bevyengine/bevy/pull/5305 -[5306]: https://github.com/bevyengine/bevy/pull/5306 -[5307]: https://github.com/bevyengine/bevy/pull/5307 -[5310]: https://github.com/bevyengine/bevy/pull/5310 -[5312]: https://github.com/bevyengine/bevy/pull/5312 -[5324]: https://github.com/bevyengine/bevy/pull/5324 -[5335]: https://github.com/bevyengine/bevy/pull/5335 -[5339]: https://github.com/bevyengine/bevy/pull/5339 -[5343]: https://github.com/bevyengine/bevy/pull/5343 -[5344]: https://github.com/bevyengine/bevy/pull/5344 -[5348]: https://github.com/bevyengine/bevy/pull/5348 -[5349]: https://github.com/bevyengine/bevy/pull/5349 -[5355]: https://github.com/bevyengine/bevy/pull/5355 -[5359]: https://github.com/bevyengine/bevy/pull/5359 -[5364]: https://github.com/bevyengine/bevy/pull/5364 -[5366]: https://github.com/bevyengine/bevy/pull/5366 -[5376]: https://github.com/bevyengine/bevy/pull/5376 -[5383]: https://github.com/bevyengine/bevy/pull/5383 -[5389]: https://github.com/bevyengine/bevy/pull/5389 -[5396]: https://github.com/bevyengine/bevy/pull/5396 - -## Version 0.7.0 (2022-04-15) - -### Added - -- [Mesh Skinning][4238] -- [Animation Player][4375] -- [Gltf animations][3751] -- [Mesh vertex buffer layouts][3959] -- [Render to a texture][3412] -- [KTX2/DDS/.basis compressed texture support][3884] -- [Audio control - play, pause, volume, speed, loop][3948] -- [Auto-label function systems with SystemTypeIdLabel][4224] -- [Query::get_many][4298] -- [Dynamic light clusters][3968] -- [Always update clusters and remove per-frame allocations][4169] -- [`ParamSet` for conflicting `SystemParam`:s][2765] -- [default() shorthand][4071] -- [use marker components for cameras instead of name strings][3635] -- [Implement `WorldQuery` derive macro][2713] -- [Implement AnyOf queries][2889] -- [Compute Pipeline Specialization][3979] -- [Make get_resource (and friends) infallible][4047] -- [bevy_pbr: Support flipping tangent space normal map y for DirectX normal maps][4433] -- [Faster view frustum culling][4181] -- [Use storage buffers for clustered forward point lights][3989] -- [Add &World as SystemParam][2923] -- [Add text wrapping support to Text2d][4347] -- [Scene Viewer to display glTF files][4183] -- [Internal Asset Hot Reloading][3966] -- [Add FocusPolicy to NodeBundle and ImageBundle][3952] -- [Allow iter combinations on queries with filters][3656] -- [bevy_render: Support overriding wgpu features and limits][3912] -- [bevy_render: Use RenderDevice to get limits/features and expose AdapterInfo][3931] -- [Reduce power usage with configurable event loop][3974] -- [can specify an anchor for a sprite][3463] -- [Implement len and is_empty for EventReaders][2969] -- [Add more FromWorld implementations][3945] -- [Add cart's fork of ecs_bench_suite][4225] -- [bevy_derive: Add derives for `Deref` and `DerefMut`][4328] -- [Add clear_schedule][3941] -- [Add Query::contains][3090] -- [bevy_render: Support removal of nodes, edges, subgraphs][3048] -- [Implement init_resource for `Commands` and `World`][3079] -- [Added method to restart the current state][3328] -- [Simplify sending empty events][2935] -- [impl Command for `impl FnOnce(&mut World)`][2996] -- [Useful error message when two assets have the save UUID][3739] -- [bevy_asset: Add AssetServerSettings watch_for_changes member][3643] -- [Add conversio from Color to u32][4088] -- [Introduce `SystemLabel`'s for `RenderAssetPlugin`, and change `Image` preparation system to run before others][3917] -- [Add a helper for storage buffers similar to `UniformVec`][4079] -- [StandardMaterial: expose a cull_mode option][3982] -- [Expose draw indirect][4056] -- [Add view transform to view uniform][3885] -- [Add a size method on Image.][3696] -- [add Visibility for lights][3958] -- [bevy_render: Provide a way to opt-out of the built-in frustum culling][3711] -- [use error scope to handle errors on shader module creation][3675] -- [include sources in shader validation error][3724] -- [insert the gltf mesh name on the entity if there is one][4119] -- [expose extras from gltf nodes][2154] -- [gltf: add a name to nodes without names][4396] -- [Enable drag-and-drop events on windows][3772] -- [Add transform hierarchy stress test][4170] -- [Add TransformBundle][3054] -- [Add Transform::rotate_around method][3107] -- [example on how to create an animation in code][4399] -- [Add examples for Transforms][2441] -- [Add mouse grab example][4114] -- [examples: add screenspace texture shader example][4063] -- [Add generic systems example][2636] -- [add examples on how to have a data source running in another thread / in a task pool thread][2915] -- [Simple 2d rotation example][3065] -- [Add move sprite example.][2414] -- [add an example using UI & states to create a game menu][2960] -- [CI runs `cargo miri test -p bevy_ecs`][4310] -- [Tracy spans around main 3D passes][4182] -- [Add automatic docs deployment to GitHub Pages][3535] - -### Changed - -- [Proper prehashing][3963] -- [Move import_path definitions into shader source][3976] -- [Make `System` responsible for updating its own archetypes][4115] -- [Some small changes related to run criteria piping][3923] -- [Remove unnecessary system labels][4340] -- [Increment last event count on next instead of iter][2382] -- [Obviate the need for `RunSystem`, and remove it][3817] -- [Cleanup some things which shouldn't be components][2982] -- [Remove the config api][3633] -- [Deprecate `.system`][3302] -- [Hide docs for concrete impls of Fetch, FetchState, and SystemParamState][4250] -- [Move the CoreStage::Startup to a seperate StartupSchedule label][2434] -- [`iter_mut` on Assets: send modified event only when asset is iterated over][3565] -- [check if resource for asset already exists before adding it][3560] -- [bevy_render: Batch insertion for prepare_uniform_components][4179] -- [Change default `ColorMaterial` color to white][3981] -- [bevy_render: Only auto-disable mappable primary buffers for discrete GPUs][3803] -- [bevy_render: Do not automatically enable MAPPABLE_PRIMARY_BUFFERS][3698] -- [increase the maximum number of point lights with shadows to the max supported by the device][4435] -- [perf: only recalculate frusta of changed lights][4086] -- [bevy_pbr: Optimize assign_lights_to_clusters][3984] -- [improve error messages for render graph runner][3930] -- [Skinned extraction speedup][4428] -- [Sprites - keep color as 4 f32][4361] -- [Change scaling mode to FixedHorizontal][4055] -- [Replace VSync with PresentMode][3812] -- [do not set cursor grab on window creation if not asked for][3617] -- [bevy_transform: Use Changed in the query for much faster transform_propagate_system][4180] -- [Split bevy_hierarchy out from bevy_transform][4168] -- [Make transform builder methods const][3045] -- [many_cubes: Add a cube pattern suitable for benchmarking culling changes][4126] -- [Make many_cubes example more interesting][4015] -- [Run tests (including doc tests) in `cargo run -p ci` command][3849] -- [Use more ergonomic span syntax][4246] - -### Fixed - -- [Remove unsound lifetime annotations on `EntityMut`][4096] -- [Remove unsound lifetime annotations on `Query` methods][4243] -- [Remove `World::components_mut`][4092] -- [unsafeify `World::entities_mut`][4093] -- [Use ManuallyDrop instead of forget in insert_resource_with_id][2947] -- [Backport soundness fix][3685] -- [Fix clicked UI nodes getting reset when hovering child nodes][4194] -- [Fix ui interactions when cursor disappears suddenly][3926] -- [Fix node update][3785] -- [Fix derive(SystemParam) macro][4400] -- [SystemParam Derive fixes][2838] -- [Do not crash if RenderDevice doesn't exist][4427] -- [Fixed case of R == G, following original conversion formula][4383] -- [Fixed the frustum-sphere collision and added tests][4035] -- [bevy_render: Fix Quad flip][3741] -- [Fix HDR asset support][3795] -- [fix cluster tiling calculations][4148] -- [bevy_pbr: Do not panic when more than 256 point lights are added the scene][3697] -- [fix issues with too many point lights][3916] -- [shader preprocessor - do not import if scope is not valid][4012] -- [support all line endings in shader preprocessor][3603] -- [Fix animation: shadow and wireframe support][4367] -- [add AnimationPlayer component only on scene roots that are also animation roots][4417] -- [Fix loading non-TriangleList meshes without normals in gltf loader][4376] -- [gltf-loader: disable backface culling if material is double-sided][4270] -- [Fix glTF perspective camera projection][4006] -- [fix mul_vec3 transformation order: should be scale -> rotate -> translate][3811] - -[2154]: https://github.com/bevyengine/bevy/pull/2154 -[2382]: https://github.com/bevyengine/bevy/pull/2382 -[2414]: https://github.com/bevyengine/bevy/pull/2414 -[2434]: https://github.com/bevyengine/bevy/pull/2434 -[2441]: https://github.com/bevyengine/bevy/pull/2441 -[2636]: https://github.com/bevyengine/bevy/pull/2636 -[2713]: https://github.com/bevyengine/bevy/pull/2713 -[2765]: https://github.com/bevyengine/bevy/pull/2765 -[2838]: https://github.com/bevyengine/bevy/pull/2838 -[2889]: https://github.com/bevyengine/bevy/pull/2889 -[2915]: https://github.com/bevyengine/bevy/pull/2915 -[2923]: https://github.com/bevyengine/bevy/pull/2923 -[2935]: https://github.com/bevyengine/bevy/pull/2935 -[2947]: https://github.com/bevyengine/bevy/pull/2947 -[2960]: https://github.com/bevyengine/bevy/pull/2960 -[2969]: https://github.com/bevyengine/bevy/pull/2969 -[2982]: https://github.com/bevyengine/bevy/pull/2982 -[2996]: https://github.com/bevyengine/bevy/pull/2996 -[3045]: https://github.com/bevyengine/bevy/pull/3045 -[3048]: https://github.com/bevyengine/bevy/pull/3048 -[3054]: https://github.com/bevyengine/bevy/pull/3054 -[3065]: https://github.com/bevyengine/bevy/pull/3065 -[3079]: https://github.com/bevyengine/bevy/pull/3079 -[3090]: https://github.com/bevyengine/bevy/pull/3090 -[3107]: https://github.com/bevyengine/bevy/pull/3107 -[3302]: https://github.com/bevyengine/bevy/pull/3302 -[3328]: https://github.com/bevyengine/bevy/pull/3328 -[3412]: https://github.com/bevyengine/bevy/pull/3412 -[3463]: https://github.com/bevyengine/bevy/pull/3463 -[3535]: https://github.com/bevyengine/bevy/pull/3535 -[3560]: https://github.com/bevyengine/bevy/pull/3560 -[3565]: https://github.com/bevyengine/bevy/pull/3565 -[3603]: https://github.com/bevyengine/bevy/pull/3603 -[3617]: https://github.com/bevyengine/bevy/pull/3617 -[3633]: https://github.com/bevyengine/bevy/pull/3633 -[3635]: https://github.com/bevyengine/bevy/pull/3635 -[3643]: https://github.com/bevyengine/bevy/pull/3643 -[3656]: https://github.com/bevyengine/bevy/pull/3656 -[3675]: https://github.com/bevyengine/bevy/pull/3675 -[3685]: https://github.com/bevyengine/bevy/pull/3685 -[3696]: https://github.com/bevyengine/bevy/pull/3696 -[3697]: https://github.com/bevyengine/bevy/pull/3697 -[3698]: https://github.com/bevyengine/bevy/pull/3698 -[3711]: https://github.com/bevyengine/bevy/pull/3711 -[3724]: https://github.com/bevyengine/bevy/pull/3724 -[3739]: https://github.com/bevyengine/bevy/pull/3739 -[3741]: https://github.com/bevyengine/bevy/pull/3741 -[3751]: https://github.com/bevyengine/bevy/pull/3751 -[3772]: https://github.com/bevyengine/bevy/pull/3772 -[3785]: https://github.com/bevyengine/bevy/pull/3785 -[3795]: https://github.com/bevyengine/bevy/pull/3795 -[3803]: https://github.com/bevyengine/bevy/pull/3803 -[3811]: https://github.com/bevyengine/bevy/pull/3811 -[3812]: https://github.com/bevyengine/bevy/pull/3812 -[3817]: https://github.com/bevyengine/bevy/pull/3817 -[3849]: https://github.com/bevyengine/bevy/pull/3849 -[3884]: https://github.com/bevyengine/bevy/pull/3884 -[3885]: https://github.com/bevyengine/bevy/pull/3885 -[3912]: https://github.com/bevyengine/bevy/pull/3912 -[3916]: https://github.com/bevyengine/bevy/pull/3916 -[3917]: https://github.com/bevyengine/bevy/pull/3917 -[3923]: https://github.com/bevyengine/bevy/pull/3923 -[3926]: https://github.com/bevyengine/bevy/pull/3926 -[3930]: https://github.com/bevyengine/bevy/pull/3930 -[3931]: https://github.com/bevyengine/bevy/pull/3931 -[3941]: https://github.com/bevyengine/bevy/pull/3941 -[3945]: https://github.com/bevyengine/bevy/pull/3945 -[3948]: https://github.com/bevyengine/bevy/pull/3948 -[3952]: https://github.com/bevyengine/bevy/pull/3952 -[3958]: https://github.com/bevyengine/bevy/pull/3958 -[3959]: https://github.com/bevyengine/bevy/pull/3959 -[3963]: https://github.com/bevyengine/bevy/pull/3963 -[3966]: https://github.com/bevyengine/bevy/pull/3966 -[3968]: https://github.com/bevyengine/bevy/pull/3968 -[3974]: https://github.com/bevyengine/bevy/pull/3974 -[3976]: https://github.com/bevyengine/bevy/pull/3976 -[3979]: https://github.com/bevyengine/bevy/pull/3979 -[3981]: https://github.com/bevyengine/bevy/pull/3981 -[3982]: https://github.com/bevyengine/bevy/pull/3982 -[3984]: https://github.com/bevyengine/bevy/pull/3984 -[3989]: https://github.com/bevyengine/bevy/pull/3989 -[4006]: https://github.com/bevyengine/bevy/pull/4006 -[4012]: https://github.com/bevyengine/bevy/pull/4012 -[4015]: https://github.com/bevyengine/bevy/pull/4015 -[4035]: https://github.com/bevyengine/bevy/pull/4035 -[4047]: https://github.com/bevyengine/bevy/pull/4047 -[4055]: https://github.com/bevyengine/bevy/pull/4055 -[4056]: https://github.com/bevyengine/bevy/pull/4056 -[4063]: https://github.com/bevyengine/bevy/pull/4063 -[4071]: https://github.com/bevyengine/bevy/pull/4071 -[4079]: https://github.com/bevyengine/bevy/pull/4079 -[4086]: https://github.com/bevyengine/bevy/pull/4086 -[4088]: https://github.com/bevyengine/bevy/pull/4088 -[4092]: https://github.com/bevyengine/bevy/pull/4092 -[4093]: https://github.com/bevyengine/bevy/pull/4093 -[4096]: https://github.com/bevyengine/bevy/pull/4096 -[4114]: https://github.com/bevyengine/bevy/pull/4114 -[4115]: https://github.com/bevyengine/bevy/pull/4115 -[4119]: https://github.com/bevyengine/bevy/pull/4119 -[4126]: https://github.com/bevyengine/bevy/pull/4126 -[4148]: https://github.com/bevyengine/bevy/pull/4148 -[4168]: https://github.com/bevyengine/bevy/pull/4168 -[4169]: https://github.com/bevyengine/bevy/pull/4169 -[4170]: https://github.com/bevyengine/bevy/pull/4170 -[4179]: https://github.com/bevyengine/bevy/pull/4179 -[4180]: https://github.com/bevyengine/bevy/pull/4180 -[4181]: https://github.com/bevyengine/bevy/pull/4181 -[4182]: https://github.com/bevyengine/bevy/pull/4182 -[4183]: https://github.com/bevyengine/bevy/pull/4183 -[4194]: https://github.com/bevyengine/bevy/pull/4194 -[4224]: https://github.com/bevyengine/bevy/pull/4224 -[4225]: https://github.com/bevyengine/bevy/pull/4225 -[4238]: https://github.com/bevyengine/bevy/pull/4238 -[4243]: https://github.com/bevyengine/bevy/pull/4243 -[4246]: https://github.com/bevyengine/bevy/pull/4246 -[4250]: https://github.com/bevyengine/bevy/pull/4250 -[4270]: https://github.com/bevyengine/bevy/pull/4270 -[4298]: https://github.com/bevyengine/bevy/pull/4298 -[4310]: https://github.com/bevyengine/bevy/pull/4310 -[4328]: https://github.com/bevyengine/bevy/pull/4328 -[4340]: https://github.com/bevyengine/bevy/pull/4340 -[4347]: https://github.com/bevyengine/bevy/pull/4347 -[4361]: https://github.com/bevyengine/bevy/pull/4361 -[4367]: https://github.com/bevyengine/bevy/pull/4367 -[4375]: https://github.com/bevyengine/bevy/pull/4375 -[4376]: https://github.com/bevyengine/bevy/pull/4376 -[4383]: https://github.com/bevyengine/bevy/pull/4383 -[4396]: https://github.com/bevyengine/bevy/pull/4396 -[4399]: https://github.com/bevyengine/bevy/pull/4399 -[4400]: https://github.com/bevyengine/bevy/pull/4400 -[4417]: https://github.com/bevyengine/bevy/pull/4417 -[4427]: https://github.com/bevyengine/bevy/pull/4427 -[4428]: https://github.com/bevyengine/bevy/pull/4428 -[4433]: https://github.com/bevyengine/bevy/pull/4433 -[4435]: https://github.com/bevyengine/bevy/pull/4435 - -## Version 0.6.0 (2022-01-08) - -### Added - -- [New Renderer][3175] -- [Clustered forward rendering][3153] -- [Frustum culling][2861] -- [Sprite Batching][3060] -- [Materials and MaterialPlugin][3428] -- [2D Meshes and Materials][3460] -- [WebGL2 support][3039] -- [Pipeline Specialization, Shader Assets, and Shader Preprocessing][3031] -- [Modular Rendering][2831] -- [Directional light and shadow][c6] -- [Directional light][2112] -- [Use the infinite reverse right-handed perspective projection][2543] -- [Implement and require `#[derive(Component)]` on all component structs][2254] -- [Shader Imports. Decouple Mesh logic from PBR][3137] -- [Add support for opaque, alpha mask, and alpha blend modes][3072] -- [bevy_gltf: Load light names from gltf][3553] -- [bevy_gltf: Add support for loading lights][3506] -- [Spherical Area Lights][1901] -- [Shader Processor: process imported shader][3290] -- [Add support for not casting/receiving shadows][2726] -- [Add support for configurable shadow map sizes][2700] -- [Implement the `Overflow::Hidden` style property for UI][3296] -- [SystemState][2283] -- [Add a method `iter_combinations` on query to iterate over combinations of query results][1763] -- [Add FromReflect trait to convert dynamic types to concrete types][1395] -- [More pipelined-rendering shader examples][3041] -- [Configurable wgpu features/limits priority][3452] -- [Cargo feature for bevy UI][3546] -- [Spherical area lights example][3498] -- [Implement ReflectValue serialization for Duration][3318] -- [bevy_ui: register Overflow type][3443] -- [Add Visibility component to UI][3426] -- [Implement non-indexed mesh rendering][3415] -- [add tracing spans for parallel executor and system overhead][3416] -- [RemoveChildren command][1925] -- [report shader processing errors in `RenderPipelineCache`][3289] -- [enable Webgl2 optimisation in pbr under feature][3291] -- [Implement Sub-App Labels][2695] -- [Added `set_cursor_icon(...)` to `Window`][3395] -- [Support topologies other than TriangleList][3349] -- [Add an example 'showcasing' using multiple windows][3367] -- [Add an example to draw a rectangle][2957] -- [Added set_scissor_rect to tracked render pass.][3320] -- [Add RenderWorld to Extract step][2555] -- [re-export ClearPassNode][3336] -- [add default standard material in PbrBundle][3325] -- [add methods to get reads and writes of `Access`][3166] -- [Add despawn_children][2903] -- [More Bevy ECS schedule spans][3281] -- [Added transparency to window builder][3105] -- [Add Gamepads resource][3257] -- [Add support for #else for shader defs][3206] -- [Implement iter() for mutable Queries][2305] -- [add shadows in examples][3201] -- [Added missing wgpu image render resources.][3171] -- [Per-light toggleable shadow mapping][3126] -- [Support nested shader defs][3113] -- [use bytemuck crate instead of Byteable trait][2183] -- [`iter_mut()` for Assets type][3118] -- [EntityRenderCommand and PhaseItemRenderCommand][3111] -- [add position to WindowDescriptor][3070] -- [Add System Command apply and RenderGraph node spans][3069] -- [Support for normal maps including from glTF models][2741] -- [MSAA example][3049] -- [Add MSAA to new renderer][3042] -- [Add support for IndexFormat::Uint16][2990] -- [Apply labels to wgpu resources for improved debugging/profiling][2912] -- [Add tracing spans around render subapp and stages][2907] -- [Add set_stencil_reference to TrackedRenderPass][2885] -- [Add despawn_recursive to EntityMut][2855] -- [Add trace_tracy feature for Tracy profiling][2832] -- [Expose wgpu's StencilOperation with bevy][2819] -- [add get_single variant][2793] -- [Add builder methods to Transform][2778] -- [add get_history function to Diagnostic][2772] -- [Add convenience methods for checking a set of inputs][2760] -- [Add error messages for the spooky insertions][2581] -- [Add Deref implementation for ComputePipeline][2759] -- [Derive thiserror::Error for HexColorError][2740] -- [Spawn specific entities: spawn or insert operations, refactor spawn internals, world clearing][2673] -- [Add ClearColor Resource to Pipelined Renderer][2631] -- [remove_component for ReflectComponent][2682] -- [Added ComputePipelineDescriptor][2628] -- [Added StorageTextureAccess to the exposed wgpu API][2614] -- [Add sprite atlases into the new renderer.][2560] -- [Log adapter info on initialization][2542] -- [Add feature flag to enable wasm for bevy_audio][2397] -- [Allow `Option>` and `Option>` as SystemParam][2345] -- [Added helpful adders for systemsets][2366] -- [Derive Clone for Time][2360] -- [Implement Clone for Fetches][2641] -- [Implement IntoSystemDescriptor for SystemDescriptor][2718] -- [implement DetectChanges for NonSendMut][2326] -- [Log errors when loading textures from a gltf file][2260] -- [expose texture/image conversions as From/TryFrom][2175] -- [[ecs] implement is_empty for queries][2271] -- [Add audio to ios example][1007] -- [Example showing how to use AsyncComputeTaskPool and Tasks][2180] -- [Expose set_changed() on ResMut and Mut][2208] -- [Impl AsRef+AsMut for Res, ResMut, and Mut][2189] -- [Add exit_on_esc_system to examples with window][2121] -- [Implement rotation for Text2d][2084] -- [Mesh vertex attributes for skinning and animation][1831] -- [load zeroed UVs as fallback in gltf loader][1803] -- [Implement direct mutable dereferencing][2100] -- [add a span for frames][2053] -- [Add an alias mouse position -> cursor position][2038] -- [Adding `WorldQuery` for `WithBundle`][2024] -- [Automatic System Spans][2033] -- [Add system sets and run criteria example][1909] -- [EnumVariantMeta derive][1972] -- [Added TryFrom for VertexAttributeValues][1963] -- [add render_to_texture example][1927] -- [Added example of entity sorting by components][1817] -- [calculate flat normals for mesh if missing][1808] -- [Add animate shaders example][1765] -- [examples on how to tests systems][1714] -- [Add a UV sphere implementation][1887] -- [Add additional vertex formats][1878] -- [gltf-loader: support data url for images][1828] -- [glTF: added color attribute support][1775] -- [Add synonyms for transform relative vectors][1667] - -### Changed - -- [Relicense Bevy under the dual MIT or Apache-2.0 license][2509] -- [[ecs] Improve `Commands` performance][2332] -- [Merge AppBuilder into App][2531] -- [Use a special first depth slice for clustered forward rendering][3545] -- [Add a separate ClearPass][3209] -- [bevy_pbr2: Improve lighting units and documentation][2704] -- [gltf loader: do not use the taskpool for only one task][3577] -- [System Param Lifetime Split][2605] -- [Optional `.system`][2398] -- [Optional `.system()`, part 2][2403] -- [Optional `.system()`, part 3][2422] -- [Optional `.system()`, part 4 (run criteria)][2431] -- [Optional `.system()`, part 6 (chaining)][2494] -- [Make the `iter_combinators` examples prettier][3075] -- [Remove dead anchor.rs code][3551] -- [gltf: load textures asynchronously using io task pool][1767] -- [Use fully-qualified type names in Label derive.][3544] -- [Remove Bytes, FromBytes, Labels, EntityLabels][3521] -- [StorageType parameter removed from ComponentDescriptor::new_resource][3495] -- [remove dead code: ShaderDefs derive][3490] -- [Enable Msaa for webgl by default][3489] -- [Renamed Entity::new to Entity::from_raw][3465] -- [bevy::scene::Entity renamed to bevy::scene::DynamicEntity.][3448] -- [make `sub_app` return an `&App` and add `sub_app_mut() -> &mut App`][3309] -- [use ogg by default instead of mp3][3421] -- [enable `wasm-bindgen` feature on gilrs][3420] -- [Use EventWriter for gilrs_system][3413] -- [Add some of the missing methods to `TrackedRenderPass`][3401] -- [Only bevy_render depends directly on wgpu][3393] -- [Update wgpu to 0.12 and naga to 0.8][3375] -- [Improved bevymark: no bouncing offscreen and spawn waves from CLI][3364] -- [Rename render UiSystem to RenderUiSystem][3371] -- [Use updated window size in bevymark example][3335] -- [Enable trace feature for subfeatures using it][3337] -- [Schedule gilrs system before input systems][2989] -- [Rename fixed timestep state and add a test][3260] -- [Port bevy_ui to pipelined-rendering][2653] -- [update wireframe rendering to new renderer][3193] -- [Allow `String` and `&String` as `Id` for `AssetServer.get_handle(id)`][3280] -- [Ported WgpuOptions to new renderer][3282] -- [Down with the system!][2496] -- [Update dependencies `ron` `winit`& fix `cargo-deny` lists][3244] -- [Improve contributors example quality][3258] -- [Expose command encoders][3271] -- [Made Time::time_since_startup return from last tick.][3264] -- [Default image used in PipelinedSpriteBundle to be able to render without loading a texture][3270] -- [make texture from sprite pipeline filterable][3236] -- [iOS: replace cargo-lipo, and update for new macOS][3109] -- [increase light intensity in pbr example][3182] -- [Faster gltf loader][3189] -- [Use crevice std140_size_static everywhere][3168] -- [replace matrix swizzles in pbr shader with index accesses][3122] -- [Disable default features from `bevy_asset` and `bevy_ecs`][3097] -- [Update tracing-subscriber requirement from 0.2.22 to 0.3.1][3076] -- [Update vendored Crevice to 0.8.0 + PR for arrays][3059] -- [change texture atlas sprite indexing to usize][2887] -- [Update derive(DynamicPlugin) to edition 2021][3038] -- [Update to edition 2021 on master][3028] -- [Add entity ID to expect() message][2943] -- [Use RenderQueue in BufferVec][2847] -- [removed unused RenderResourceId and SwapChainFrame][2890] -- [Unique WorldId][2827] -- [add_texture returns index to texture][2864] -- [Update hexasphere requirement from 4.0.0 to 5.0.0][2880] -- [enable change detection for hierarchy maintenance][2411] -- [Make events reuse buffers][2850] -- [Replace `.insert_resource(T::default())` calls with `init_resource::()`][2807] -- [Improve many sprites example][2785] -- [Update glam requirement from 0.17.3 to 0.18.0][2748] -- [update ndk-glue to 0.4][2684] -- [Remove Need for Sprite Size Sync System][2632] -- [Pipelined separate shadow vertex shader][2727] -- [Sub app label changes][2717] -- [Use Explicit Names for Flex Direction][2672] -- [Make default near plane more sensible at 0.1][2703] -- [Reduce visibility of various types and fields][2690] -- [Cleanup FromResources][2601] -- [Better error message for unsupported shader features Fixes #869][2598] -- [Change definition of `ScheduleRunnerPlugin`][2606] -- [Re-implement Automatic Sprite Sizing][2613] -- [Remove with bundle filter][2623] -- [Remove bevy_dynamic_plugin as a default][2578] -- [Port bevy_gltf to pipelined-rendering][2537] -- [Bump notify to 5.0.0-pre.11][2564] -- [Add 's (state) lifetime to `Fetch`][2515] -- [move bevy_core_pipeline to its own plugin][2552] -- [Refactor ECS to reduce the dependency on a 1-to-1 mapping between components and real rust types][2490] -- [Inline world get][2520] -- [Dedupe move logic in remove_bundle and remove_bundle_intersection][2521] -- [remove .system from pipelined code][2538] -- [Scale normal bias by texel size][c26] -- [Make Remove Command's fields public][2449] -- [bevy_utils: Re-introduce `with_capacity()`.][2393] -- [Update rodio requirement from 0.13 to 0.14][2244] -- [Optimize Events::extend and impl std::iter::Extend][2207] -- [Bump winit to 0.25][2186] -- [Improve legibility of RunOnce::run_unsafe param][2181] -- [Update gltf requirement from 0.15.2 to 0.16.0][2196] -- [Move to smallvec v1.6][2074] -- [Update rectangle-pack requirement from 0.3 to 0.4][2086] -- [Make Commands public?][2034] -- [Monomorphize various things][1914] -- [Detect camera projection changes][2015] -- [support assets of any size][1997] -- [Separate Query filter access from fetch access during initial evaluation][1977] -- [Provide better error message when missing a render backend][1965] -- [par_for_each: split batches when iterating on a sparse query][1945] -- [Allow deriving `SystemParam` on private types][1936] -- [Angle bracket annotated types to support generics][1919] -- [More detailed errors when resource not found][1864] -- [Moved events to ECS][1823] -- [Use a sorted Map for vertex buffer attributes][1796] -- [Error message improvements for shader compilation/gltf loading][1786] -- [Rename Light => PointLight and remove unused properties][1778] -- [Override size_hint for all Iterators and add ExactSizeIterator where applicable][1734] -- [Change breakout to use fixed timestamp][1541] - -### Fixed - -- [Fix shadows for non-TriangleLists][3581] -- [Fix error message for the `Component` macro's `component` `storage` attribute.][3534] -- [do not add plugin ExtractComponentPlugin twice for StandardMaterial][3502] -- [load spirv using correct API][3466] -- [fix shader compilation error reporting for non-wgsl shaders][3441] -- [bevy_ui: Check clip when handling interactions][3461] -- [crevice derive macro: fix path to render_resource when importing from bevy][3438] -- [fix parenting of scenes][2410] -- [Do not panic on failed setting of GameOver state in AlienCakeAddict][3411] -- [Fix minimization crash because of cluster updates.][3369] -- [Fix custom mesh pipelines][3381] -- [Fix hierarchy example panic][3378] -- [Fix double drop in BlobVec::replace_unchecked (#2597)][2848] -- [Remove vestigial derives][3343] -- [Fix crash with disabled winit][3330] -- [Fix clustering for orthographic projections][3316] -- [Run a clear pass on Windows without any Views][3304] -- [Remove some superfluous unsafe code][3297] -- [clearpass: also clear views without depth (2d)][3286] -- [Check for NaN in `Camera::world_to_screen()`][3268] -- [Fix sprite hot reloading in new renderer][3207] -- [Fix path used by macro not considering that we can use a sub-crate][3178] -- [Fix torus normals][3549] -- [enable alpha mode for textures materials that are transparent][3202] -- [fix calls to as_rgba_linear][3200] -- [Fix shadow logic][3186] -- [fix: as_rgba_linear used wrong variant][3192] -- [Fix MIME type support for glTF buffer Data URIs][3101] -- [Remove wasm audio feature flag for 2021][3000] -- [use correct size of pixel instead of 4][2977] -- [Fix custom_shader_pipelined example shader][2992] -- [Fix scale factor for cursor position][2932] -- [fix window resize after wgpu 0.11 upgrade][2953] -- [Fix unsound lifetime annotation on `Query::get_component`][2964] -- [Remove double Events::update in bevy-gilrs][2894] -- [Fix bevy_ecs::schedule::executor_parallel::system span management][2905] -- [Avoid some format! into immediate format!][2913] -- [Fix panic on is_resource_* calls (#2828)][2863] -- [Fix window size change panic][2858] -- [fix `Default` implementation of `Image` so that size and data match][2833] -- [Fix scale_factor_override in the winit backend][2784] -- [Fix breakout example scoreboard][2770] -- [Fix `Option>` and `Option>`][2757] -- [fix missing paths in ECS SystemParam derive macro v2][2550] -- [Add missing bytemuck feature][2625] -- [Update EntityMut's location in push_children() and insert_children()][2604] -- [Fixed issue with how texture arrays were uploaded with write_texture.][c24] -- [Don't update when suspended to avoid GPU use on iOS.][2482] -- [update archetypes for run criterias][2177] -- [Fix AssetServer::get_asset_loader deadlock][2395] -- [Fix unsetting RenderLayers bit in without fn][2409] -- [Fix view vector in pbr frag to work in ortho][2370] -- [Fixes Timer Precision Error Causing Panic][2362] -- [[assets] Fix `AssetServer::get_handle_path`][2310] -- [Fix bad bounds for NonSend SystemParams][2325] -- [Add minimum sizes to textures to prevent crash][2300] -- [[assets] set LoadState properly and more testing!][2226] -- [[assets] properly set `LoadState` with invalid asset extension][2318] -- [Fix Bevy crashing if no audio device is found][2269] -- [Fixes dropping empty BlobVec][2295] -- [[assets] fix Assets being set as 'changed' each frame][2280] -- [drop overwritten component data on double insert][2227] -- [Despawn with children doesn't need to remove entities from parents children when parents are also removed][2278] -- [reduce tricky unsafety and simplify table structure][2221] -- [Use bevy_reflect as path in case of no direct references][1875] -- [Fix Events:: bug][2206] -- [small ecs cleanup and remove_bundle drop bugfix][2172] -- [Fix PBR regression for unlit materials][2197] -- [prevent memory leak when dropping ParallelSystemContainer][2176] -- [fix diagnostic length for asset count][2165] -- [Fixes incorrect `PipelineCompiler::compile_pipeline()` step_mode][2126] -- [Asset re-loading while it's being deleted][2011] -- [Bevy derives handling generics in impl definitions.][2044] -- [Fix unsoundness in `Query::for_each_mut`][2045] -- [Fix mesh with no vertex attributes causing panic][2036] -- [Fix alien_cake_addict: cake should not be at height of player's location][1954] -- [fix memory size for PointLightBundle][1940] -- [Fix unsoundness in query component access][1929] -- [fixing compilation error on macos aarch64][1905] -- [Fix SystemParam handling of Commands][1899] -- [Fix IcoSphere UV coordinates][1871] -- [fix 'attempted to subtract with overflow' for State::inactives][1668] - -[1007]: https://github.com/bevyengine/bevy/pull/1007 -[1395]: https://github.com/bevyengine/bevy/pull/1395 -[1541]: https://github.com/bevyengine/bevy/pull/1541 -[1667]: https://github.com/bevyengine/bevy/pull/1667 -[1668]: https://github.com/bevyengine/bevy/pull/1668 -[1714]: https://github.com/bevyengine/bevy/pull/1714 -[1734]: https://github.com/bevyengine/bevy/pull/1734 -[1763]: https://github.com/bevyengine/bevy/pull/1763 -[1765]: https://github.com/bevyengine/bevy/pull/1765 -[1767]: https://github.com/bevyengine/bevy/pull/1767 -[1775]: https://github.com/bevyengine/bevy/pull/1775 -[1778]: https://github.com/bevyengine/bevy/pull/1778 -[1786]: https://github.com/bevyengine/bevy/pull/1786 -[1796]: https://github.com/bevyengine/bevy/pull/1796 -[1803]: https://github.com/bevyengine/bevy/pull/1803 -[1808]: https://github.com/bevyengine/bevy/pull/1808 -[1817]: https://github.com/bevyengine/bevy/pull/1817 -[1823]: https://github.com/bevyengine/bevy/pull/1823 -[1828]: https://github.com/bevyengine/bevy/pull/1828 -[1831]: https://github.com/bevyengine/bevy/pull/1831 -[1864]: https://github.com/bevyengine/bevy/pull/1864 -[1871]: https://github.com/bevyengine/bevy/pull/1871 -[1875]: https://github.com/bevyengine/bevy/pull/1875 -[1878]: https://github.com/bevyengine/bevy/pull/1878 -[1887]: https://github.com/bevyengine/bevy/pull/1887 -[1899]: https://github.com/bevyengine/bevy/pull/1899 -[1901]: https://github.com/bevyengine/bevy/pull/1901 -[1905]: https://github.com/bevyengine/bevy/pull/1905 -[1909]: https://github.com/bevyengine/bevy/pull/1909 -[1914]: https://github.com/bevyengine/bevy/pull/1914 -[1919]: https://github.com/bevyengine/bevy/pull/1919 -[1925]: https://github.com/bevyengine/bevy/pull/1925 -[1927]: https://github.com/bevyengine/bevy/pull/1927 -[1929]: https://github.com/bevyengine/bevy/pull/1929 -[1936]: https://github.com/bevyengine/bevy/pull/1936 -[1940]: https://github.com/bevyengine/bevy/pull/1940 -[1945]: https://github.com/bevyengine/bevy/pull/1945 -[1954]: https://github.com/bevyengine/bevy/pull/1954 -[1963]: https://github.com/bevyengine/bevy/pull/1963 -[1965]: https://github.com/bevyengine/bevy/pull/1965 -[1972]: https://github.com/bevyengine/bevy/pull/1972 -[1977]: https://github.com/bevyengine/bevy/pull/1977 -[1997]: https://github.com/bevyengine/bevy/pull/1997 -[2011]: https://github.com/bevyengine/bevy/pull/2011 -[2015]: https://github.com/bevyengine/bevy/pull/2015 -[2024]: https://github.com/bevyengine/bevy/pull/2024 -[2033]: https://github.com/bevyengine/bevy/pull/2033 -[2034]: https://github.com/bevyengine/bevy/pull/2034 -[2036]: https://github.com/bevyengine/bevy/pull/2036 -[2038]: https://github.com/bevyengine/bevy/pull/2038 -[2044]: https://github.com/bevyengine/bevy/pull/2044 -[2045]: https://github.com/bevyengine/bevy/pull/2045 -[2053]: https://github.com/bevyengine/bevy/pull/2053 -[2074]: https://github.com/bevyengine/bevy/pull/2074 -[2084]: https://github.com/bevyengine/bevy/pull/2084 -[2086]: https://github.com/bevyengine/bevy/pull/2086 -[2100]: https://github.com/bevyengine/bevy/pull/2100 -[2112]: https://github.com/bevyengine/bevy/pull/2112 -[2121]: https://github.com/bevyengine/bevy/pull/2121 -[2126]: https://github.com/bevyengine/bevy/pull/2126 -[2165]: https://github.com/bevyengine/bevy/pull/2165 -[2172]: https://github.com/bevyengine/bevy/pull/2172 -[2175]: https://github.com/bevyengine/bevy/pull/2175 -[2176]: https://github.com/bevyengine/bevy/pull/2176 -[2177]: https://github.com/bevyengine/bevy/pull/2177 -[2180]: https://github.com/bevyengine/bevy/pull/2180 -[2181]: https://github.com/bevyengine/bevy/pull/2181 -[2183]: https://github.com/bevyengine/bevy/pull/2183 -[2186]: https://github.com/bevyengine/bevy/pull/2186 -[2189]: https://github.com/bevyengine/bevy/pull/2189 -[2196]: https://github.com/bevyengine/bevy/pull/2196 -[2197]: https://github.com/bevyengine/bevy/pull/2197 -[2206]: https://github.com/bevyengine/bevy/pull/2206 -[2207]: https://github.com/bevyengine/bevy/pull/2207 -[2208]: https://github.com/bevyengine/bevy/pull/2208 -[2221]: https://github.com/bevyengine/bevy/pull/2221 -[2226]: https://github.com/bevyengine/bevy/pull/2226 -[2227]: https://github.com/bevyengine/bevy/pull/2227 -[2244]: https://github.com/bevyengine/bevy/pull/2244 -[2254]: https://github.com/bevyengine/bevy/pull/2254 -[2260]: https://github.com/bevyengine/bevy/pull/2260 -[2269]: https://github.com/bevyengine/bevy/pull/2269 -[2271]: https://github.com/bevyengine/bevy/pull/2271 -[2278]: https://github.com/bevyengine/bevy/pull/2278 -[2280]: https://github.com/bevyengine/bevy/pull/2280 -[2283]: https://github.com/bevyengine/bevy/pull/2283 -[2295]: https://github.com/bevyengine/bevy/pull/2295 -[2300]: https://github.com/bevyengine/bevy/pull/2300 -[2305]: https://github.com/bevyengine/bevy/pull/2305 -[2310]: https://github.com/bevyengine/bevy/pull/2310 -[2318]: https://github.com/bevyengine/bevy/pull/2318 -[2325]: https://github.com/bevyengine/bevy/pull/2325 -[2326]: https://github.com/bevyengine/bevy/pull/2326 -[2332]: https://github.com/bevyengine/bevy/pull/2332 -[2345]: https://github.com/bevyengine/bevy/pull/2345 -[2360]: https://github.com/bevyengine/bevy/pull/2360 -[2362]: https://github.com/bevyengine/bevy/pull/2362 -[2366]: https://github.com/bevyengine/bevy/pull/2366 -[2370]: https://github.com/bevyengine/bevy/pull/2370 -[2393]: https://github.com/bevyengine/bevy/pull/2393 -[2395]: https://github.com/bevyengine/bevy/pull/2395 -[2397]: https://github.com/bevyengine/bevy/pull/2397 -[2398]: https://github.com/bevyengine/bevy/pull/2398 -[2403]: https://github.com/bevyengine/bevy/pull/2403 -[2409]: https://github.com/bevyengine/bevy/pull/2409 -[2410]: https://github.com/bevyengine/bevy/pull/2410 -[2411]: https://github.com/bevyengine/bevy/pull/2411 -[2422]: https://github.com/bevyengine/bevy/pull/2422 -[2431]: https://github.com/bevyengine/bevy/pull/2431 -[2449]: https://github.com/bevyengine/bevy/pull/2449 -[2482]: https://github.com/bevyengine/bevy/pull/2482 -[2490]: https://github.com/bevyengine/bevy/pull/2490 -[2494]: https://github.com/bevyengine/bevy/pull/2494 -[2496]: https://github.com/bevyengine/bevy/pull/2496 -[2509]: https://github.com/bevyengine/bevy/pull/2509 -[2515]: https://github.com/bevyengine/bevy/pull/2515 -[2520]: https://github.com/bevyengine/bevy/pull/2520 -[2521]: https://github.com/bevyengine/bevy/pull/2521 -[2531]: https://github.com/bevyengine/bevy/pull/2531 -[2537]: https://github.com/bevyengine/bevy/pull/2537 -[2538]: https://github.com/bevyengine/bevy/pull/2538 -[2542]: https://github.com/bevyengine/bevy/pull/2542 -[2543]: https://github.com/bevyengine/bevy/pull/2543 -[2550]: https://github.com/bevyengine/bevy/pull/2550 -[2552]: https://github.com/bevyengine/bevy/pull/2552 -[2555]: https://github.com/bevyengine/bevy/pull/2555 -[2560]: https://github.com/bevyengine/bevy/pull/2560 -[2564]: https://github.com/bevyengine/bevy/pull/2564 -[2578]: https://github.com/bevyengine/bevy/pull/2578 -[2581]: https://github.com/bevyengine/bevy/pull/2581 -[2598]: https://github.com/bevyengine/bevy/pull/2598 -[2601]: https://github.com/bevyengine/bevy/pull/2601 -[2604]: https://github.com/bevyengine/bevy/pull/2604 -[2605]: https://github.com/bevyengine/bevy/pull/2605 -[2606]: https://github.com/bevyengine/bevy/pull/2606 -[2613]: https://github.com/bevyengine/bevy/pull/2613 -[2614]: https://github.com/bevyengine/bevy/pull/2614 -[2623]: https://github.com/bevyengine/bevy/pull/2623 -[2625]: https://github.com/bevyengine/bevy/pull/2625 -[2628]: https://github.com/bevyengine/bevy/pull/2628 -[2631]: https://github.com/bevyengine/bevy/pull/2631 -[2632]: https://github.com/bevyengine/bevy/pull/2632 -[2641]: https://github.com/bevyengine/bevy/pull/2641 -[2653]: https://github.com/bevyengine/bevy/pull/2653 -[2672]: https://github.com/bevyengine/bevy/pull/2672 -[2673]: https://github.com/bevyengine/bevy/pull/2673 -[2682]: https://github.com/bevyengine/bevy/pull/2682 -[2684]: https://github.com/bevyengine/bevy/pull/2684 -[2690]: https://github.com/bevyengine/bevy/pull/2690 -[2695]: https://github.com/bevyengine/bevy/pull/2695 -[2700]: https://github.com/bevyengine/bevy/pull/2700 -[2703]: https://github.com/bevyengine/bevy/pull/2703 -[2704]: https://github.com/bevyengine/bevy/pull/2704 -[2717]: https://github.com/bevyengine/bevy/pull/2717 -[2718]: https://github.com/bevyengine/bevy/pull/2718 -[2726]: https://github.com/bevyengine/bevy/pull/2726 -[2727]: https://github.com/bevyengine/bevy/pull/2727 -[2740]: https://github.com/bevyengine/bevy/pull/2740 -[2741]: https://github.com/bevyengine/bevy/pull/2741 -[2748]: https://github.com/bevyengine/bevy/pull/2748 -[2757]: https://github.com/bevyengine/bevy/pull/2757 -[2759]: https://github.com/bevyengine/bevy/pull/2759 -[2760]: https://github.com/bevyengine/bevy/pull/2760 -[2770]: https://github.com/bevyengine/bevy/pull/2770 -[2772]: https://github.com/bevyengine/bevy/pull/2772 -[2778]: https://github.com/bevyengine/bevy/pull/2778 -[2784]: https://github.com/bevyengine/bevy/pull/2784 -[2785]: https://github.com/bevyengine/bevy/pull/2785 -[2793]: https://github.com/bevyengine/bevy/pull/2793 -[2807]: https://github.com/bevyengine/bevy/pull/2807 -[2819]: https://github.com/bevyengine/bevy/pull/2819 -[2827]: https://github.com/bevyengine/bevy/pull/2827 -[2831]: https://github.com/bevyengine/bevy/pull/2831 -[2832]: https://github.com/bevyengine/bevy/pull/2832 -[2833]: https://github.com/bevyengine/bevy/pull/2833 -[2847]: https://github.com/bevyengine/bevy/pull/2847 -[2848]: https://github.com/bevyengine/bevy/pull/2848 -[2850]: https://github.com/bevyengine/bevy/pull/2850 -[2855]: https://github.com/bevyengine/bevy/pull/2855 -[2858]: https://github.com/bevyengine/bevy/pull/2858 -[2861]: https://github.com/bevyengine/bevy/pull/2861 -[2863]: https://github.com/bevyengine/bevy/pull/2863 -[2864]: https://github.com/bevyengine/bevy/pull/2864 -[2880]: https://github.com/bevyengine/bevy/pull/2880 -[2885]: https://github.com/bevyengine/bevy/pull/2885 -[2887]: https://github.com/bevyengine/bevy/pull/2887 -[2890]: https://github.com/bevyengine/bevy/pull/2890 -[2894]: https://github.com/bevyengine/bevy/pull/2894 -[2903]: https://github.com/bevyengine/bevy/pull/2903 -[2905]: https://github.com/bevyengine/bevy/pull/2905 -[2907]: https://github.com/bevyengine/bevy/pull/2907 -[2912]: https://github.com/bevyengine/bevy/pull/2912 -[2913]: https://github.com/bevyengine/bevy/pull/2913 -[2932]: https://github.com/bevyengine/bevy/pull/2932 -[2943]: https://github.com/bevyengine/bevy/pull/2943 -[2953]: https://github.com/bevyengine/bevy/pull/2953 -[2957]: https://github.com/bevyengine/bevy/pull/2957 -[2964]: https://github.com/bevyengine/bevy/pull/2964 -[2977]: https://github.com/bevyengine/bevy/pull/2977 -[2989]: https://github.com/bevyengine/bevy/pull/2989 -[2990]: https://github.com/bevyengine/bevy/pull/2990 -[2992]: https://github.com/bevyengine/bevy/pull/2992 -[3000]: https://github.com/bevyengine/bevy/pull/3000 -[3028]: https://github.com/bevyengine/bevy/pull/3028 -[3031]: https://github.com/bevyengine/bevy/pull/3031 -[3038]: https://github.com/bevyengine/bevy/pull/3038 -[3039]: https://github.com/bevyengine/bevy/pull/3039 -[3041]: https://github.com/bevyengine/bevy/pull/3041 -[3042]: https://github.com/bevyengine/bevy/pull/3042 -[3049]: https://github.com/bevyengine/bevy/pull/3049 -[3059]: https://github.com/bevyengine/bevy/pull/3059 -[3060]: https://github.com/bevyengine/bevy/pull/3060 -[3069]: https://github.com/bevyengine/bevy/pull/3069 -[3070]: https://github.com/bevyengine/bevy/pull/3070 -[3072]: https://github.com/bevyengine/bevy/pull/3072 -[3075]: https://github.com/bevyengine/bevy/pull/3075 -[3076]: https://github.com/bevyengine/bevy/pull/3076 -[3097]: https://github.com/bevyengine/bevy/pull/3097 -[3101]: https://github.com/bevyengine/bevy/pull/3101 -[3105]: https://github.com/bevyengine/bevy/pull/3105 -[3109]: https://github.com/bevyengine/bevy/pull/3109 -[3111]: https://github.com/bevyengine/bevy/pull/3111 -[3113]: https://github.com/bevyengine/bevy/pull/3113 -[3118]: https://github.com/bevyengine/bevy/pull/3118 -[3122]: https://github.com/bevyengine/bevy/pull/3122 -[3126]: https://github.com/bevyengine/bevy/pull/3126 -[3137]: https://github.com/bevyengine/bevy/pull/3137 -[3153]: https://github.com/bevyengine/bevy/pull/3153 -[3166]: https://github.com/bevyengine/bevy/pull/3166 -[3168]: https://github.com/bevyengine/bevy/pull/3168 -[3171]: https://github.com/bevyengine/bevy/pull/3171 -[3175]: https://github.com/bevyengine/bevy/pull/3175 -[3178]: https://github.com/bevyengine/bevy/pull/3178 -[3182]: https://github.com/bevyengine/bevy/pull/3182 -[3186]: https://github.com/bevyengine/bevy/pull/3186 -[3189]: https://github.com/bevyengine/bevy/pull/3189 -[3192]: https://github.com/bevyengine/bevy/pull/3192 -[3193]: https://github.com/bevyengine/bevy/pull/3193 -[3200]: https://github.com/bevyengine/bevy/pull/3200 -[3201]: https://github.com/bevyengine/bevy/pull/3201 -[3202]: https://github.com/bevyengine/bevy/pull/3202 -[3206]: https://github.com/bevyengine/bevy/pull/3206 -[3207]: https://github.com/bevyengine/bevy/pull/3207 -[3209]: https://github.com/bevyengine/bevy/pull/3209 -[3236]: https://github.com/bevyengine/bevy/pull/3236 -[3244]: https://github.com/bevyengine/bevy/pull/3244 -[3257]: https://github.com/bevyengine/bevy/pull/3257 -[3258]: https://github.com/bevyengine/bevy/pull/3258 -[3260]: https://github.com/bevyengine/bevy/pull/3260 -[3264]: https://github.com/bevyengine/bevy/pull/3264 -[3268]: https://github.com/bevyengine/bevy/pull/3268 -[3270]: https://github.com/bevyengine/bevy/pull/3270 -[3271]: https://github.com/bevyengine/bevy/pull/3271 -[3280]: https://github.com/bevyengine/bevy/pull/3280 -[3281]: https://github.com/bevyengine/bevy/pull/3281 -[3282]: https://github.com/bevyengine/bevy/pull/3282 -[3286]: https://github.com/bevyengine/bevy/pull/3286 -[3289]: https://github.com/bevyengine/bevy/pull/3289 -[3290]: https://github.com/bevyengine/bevy/pull/3290 -[3291]: https://github.com/bevyengine/bevy/pull/3291 -[3296]: https://github.com/bevyengine/bevy/pull/3296 -[3297]: https://github.com/bevyengine/bevy/pull/3297 -[3304]: https://github.com/bevyengine/bevy/pull/3304 -[3309]: https://github.com/bevyengine/bevy/pull/3309 -[3316]: https://github.com/bevyengine/bevy/pull/3316 -[3318]: https://github.com/bevyengine/bevy/pull/3318 -[3320]: https://github.com/bevyengine/bevy/pull/3320 -[3325]: https://github.com/bevyengine/bevy/pull/3325 -[3330]: https://github.com/bevyengine/bevy/pull/3330 -[3335]: https://github.com/bevyengine/bevy/pull/3335 -[3336]: https://github.com/bevyengine/bevy/pull/3336 -[3337]: https://github.com/bevyengine/bevy/pull/3337 -[3343]: https://github.com/bevyengine/bevy/pull/3343 -[3349]: https://github.com/bevyengine/bevy/pull/3349 -[3364]: https://github.com/bevyengine/bevy/pull/3364 -[3367]: https://github.com/bevyengine/bevy/pull/3367 -[3369]: https://github.com/bevyengine/bevy/pull/3369 -[3371]: https://github.com/bevyengine/bevy/pull/3371 -[3375]: https://github.com/bevyengine/bevy/pull/3375 -[3378]: https://github.com/bevyengine/bevy/pull/3378 -[3381]: https://github.com/bevyengine/bevy/pull/3381 -[3393]: https://github.com/bevyengine/bevy/pull/3393 -[3395]: https://github.com/bevyengine/bevy/pull/3395 -[3401]: https://github.com/bevyengine/bevy/pull/3401 -[3411]: https://github.com/bevyengine/bevy/pull/3411 -[3413]: https://github.com/bevyengine/bevy/pull/3413 -[3415]: https://github.com/bevyengine/bevy/pull/3415 -[3416]: https://github.com/bevyengine/bevy/pull/3416 -[3420]: https://github.com/bevyengine/bevy/pull/3420 -[3421]: https://github.com/bevyengine/bevy/pull/3421 -[3426]: https://github.com/bevyengine/bevy/pull/3426 -[3428]: https://github.com/bevyengine/bevy/pull/3428 -[3438]: https://github.com/bevyengine/bevy/pull/3438 -[3441]: https://github.com/bevyengine/bevy/pull/3441 -[3443]: https://github.com/bevyengine/bevy/pull/3443 -[3448]: https://github.com/bevyengine/bevy/pull/3448 -[3452]: https://github.com/bevyengine/bevy/pull/3452 -[3460]: https://github.com/bevyengine/bevy/pull/3460 -[3461]: https://github.com/bevyengine/bevy/pull/3461 -[3465]: https://github.com/bevyengine/bevy/pull/3465 -[3466]: https://github.com/bevyengine/bevy/pull/3466 -[3489]: https://github.com/bevyengine/bevy/pull/3489 -[3490]: https://github.com/bevyengine/bevy/pull/3490 -[3495]: https://github.com/bevyengine/bevy/pull/3495 -[3498]: https://github.com/bevyengine/bevy/pull/3498 -[3502]: https://github.com/bevyengine/bevy/pull/3502 -[3506]: https://github.com/bevyengine/bevy/pull/3506 -[3521]: https://github.com/bevyengine/bevy/pull/3521 -[3534]: https://github.com/bevyengine/bevy/pull/3534 -[3544]: https://github.com/bevyengine/bevy/pull/3544 -[3545]: https://github.com/bevyengine/bevy/pull/3545 -[3546]: https://github.com/bevyengine/bevy/pull/3546 -[3549]: https://github.com/bevyengine/bevy/pull/3549 -[3551]: https://github.com/bevyengine/bevy/pull/3551 -[3553]: https://github.com/bevyengine/bevy/pull/3553 -[3577]: https://github.com/bevyengine/bevy/pull/3577 -[3581]: https://github.com/bevyengine/bevy/pull/3581 -[c6]: https://github.com/cart/bevy/pull/6 -[c24]: https://github.com/cart/bevy/pull/24 -[c26]: https://github.com/cart/bevy/pull/26 - -## Version 0.5.0 (2021-04-06) - -### Added - -- [PBR Rendering][1554] -- [PBR Textures][1632] -- [HIDPI Text][1132] -- [Rich text][1245] -- [Wireframe Rendering Pipeline][562] -- [Render Layers][1209] -- [Add Sprite Flipping][1407] -- [OrthographicProjection scaling mode + camera bundle refactoring][400] -- [3D OrthographicProjection improvements + new example][1361] -- [Flexible camera bindings][1689] -- [Render text in 2D scenes][1122] -- [`Text2d` render quality][1171] -- [System sets and run criteria v2][1675] -- [System sets and parallel executor v2][1144] -- [Many-to-many system labels][1576] -- [Non-string labels (#1423 continued)][1473] -- [Make `EventReader` a `SystemParam`][1244] -- [Add `EventWriter`][1575] -- [Reliable change detection][1471] -- [Redo State architecture][1424] -- [`Query::get_unique`][1263] -- [gltf: load normal and occlusion as linear textures][1762] -- [Add separate brightness field to AmbientLight][1605] -- [world coords to screen space][1258] -- [Experimental Frustum Culling (for Sprites)][1492] -- [Enable wgpu device limits][1544] -- [bevy_render: add torus and capsule shape][1223] -- [New mesh attribute: color][1194] -- [Minimal change to support instanced rendering][1262] -- [Add support for reading from mapped buffers][1274] -- [Texture atlas format and conversion][1365] -- [enable wgpu device features][547] -- [Subpixel text positioning][1196] -- [make more information available from loaded GLTF model][1020] -- [use `Name` on node when loading a gltf file][1183] -- [GLTF loader: support mipmap filters][1639] -- [Add support for gltf::Material::unlit][1341] -- [Implement `Reflect` for tuples up to length 12][1218] -- [Process Asset File Extensions With Multiple Dots][1277] -- [Update Scene Example to Use scn.ron File][1339] -- [3d game example][1252] -- [Add keyboard modifier example (#1656)][1657] -- [Count number of times a repeating Timer wraps around in a tick][1112] -- [recycle `Timer` refactor to duration.sparkles Add `Stopwatch` struct.][1151] -- [add scene instance entity iteration][1058] -- [Make `Commands` and `World` apis consistent][1703] -- [Add `insert_children` and `push_children` to `EntityMut`][1728] -- [Extend `AppBuilder` api with `add_system_set` and similar methods][1453] -- [add labels and ordering for transform and parent systems in `POST_UPDATE` stage][1456] -- [Explicit execution order ambiguities API][1469] -- [Resolve (most) internal system ambiguities][1606] -- [Change 'components' to 'bundles' where it makes sense semantically][1257] -- [add `Flags` as a query to get flags of component][1172] -- [Rename `add_resource` to `insert_resource`][1356] -- [Update `init_resource` to not overwrite][1349] -- [Enable dynamic mutable access to component data][1284] -- [Get rid of `ChangedRes`][1313] -- [impl `SystemParam` for `Option>` / `Option>`][1494] -- [Add Window Resize Constraints][1409] -- [Add basic file drag and drop support][1096] -- [Modify Derive to allow unit structs for `RenderResources`.][1089] -- [bevy_render: load .spv assets][1104] -- [Expose wgpu backend in WgpuOptions and allow it to be configured from the environment][1042] -- [updates on diagnostics (log + new diagnostics)][1085] -- [enable change detection for labels][1155] -- [Name component with fast comparisons][1109] -- [Support for `!Send` tasks][1216] -- [Add missing `spawn_local` method to `Scope` in the single threaded executor case][1266] -- [Add bmp as a supported texture format][1081] -- [Add an alternative winit runner that can be started when not on the main thread][1063] -- [Added `use_dpi` setting to `WindowDescriptor`][1131] -- [Implement `Copy` for `ElementState`][1154] -- [Mutable mesh accessors: `indices_mut` and `attribute_mut`][1164] -- [Add support for OTF fonts][1200] -- [Add `from_xyz` to `Transform`][1212] -- [Adding `copy_texture_to_buffer` and `copy_texture_to_texture`][1236] -- [Added `set_minimized` and `set_position` to `Window`][1292] -- [Example for 2D Frustum Culling][1503] -- [Add remove resource to commands][1478] - -### Changed - -- [Bevy ECS V2][1525] -- [Fix Reflect serialization of tuple structs][1366] -- [color spaces and representation][1572] -- [Make vertex buffers optional][1485] -- [add to lower case to make asset loading case insensitive][1427] -- [Replace right/up/forward and counter parts with `local_x`/`local_y` and `local_z`][1476] -- [Use valid keys to initialize `AHasher` in `FixedState`][1268] -- [Change `Name` to take `Into` instead of `String`][1283] -- [Update to wgpu-rs 0.7][542] -- [Update glam to 0.13.0.][1550] -- [use std clamp instead of Bevy's][1644] -- [Make `Reflect` impls unsafe (`Reflect::any` must return `self`)][1679] - -### Fixed - -- [convert grayscale images to rgb][1524] -- [Glb textures should use bevy_render to load images][1454] -- [Don't panic on error when loading assets][1286] -- [Prevent ImageBundles from causing constant layout recalculations][1299] -- [do not check for focus until cursor position has been set][1070] -- [Fix lock order to remove the chance of deadlock][1121] -- [Prevent double panic in the Drop of TaksPoolInner][1064] -- [Ignore events when receiving unknown WindowId][1072] -- [Fix potential bug when using multiple lights.][1055] -- [remove panics when mixing UI and non UI entities in hierarchy][1180] -- [fix label to load gltf scene][1204] -- [fix repeated gamepad events][1221] -- [Fix iOS touch location][1224] -- [Don't panic if there's no index buffer and call draw][1229] -- [Fix Bug in Asset Server Error Message Formatter][1340] -- [add_stage now checks Stage existence][1346] -- [Fix Un-Renamed add_resource Compile Error][1357] -- [Fix Interaction not resetting to None sometimes][1315] -- [Fix regression causing "flipped" sprites to be invisible][1399] -- [revert default vsync mode to Fifo][1416] -- [Fix missing paths in ECS SystemParam derive macro][1434] -- [Fix staging buffer required size calculation (fixes #1056)][1509] - -[400]: https://github.com/bevyengine/bevy/pull/400 -[542]: https://github.com/bevyengine/bevy/pull/542 -[547]: https://github.com/bevyengine/bevy/pull/547 -[562]: https://github.com/bevyengine/bevy/pull/562 -[1020]: https://github.com/bevyengine/bevy/pull/1020 -[1042]: https://github.com/bevyengine/bevy/pull/1042 -[1055]: https://github.com/bevyengine/bevy/pull/1055 -[1058]: https://github.com/bevyengine/bevy/pull/1058 -[1063]: https://github.com/bevyengine/bevy/pull/1063 -[1064]: https://github.com/bevyengine/bevy/pull/1064 -[1070]: https://github.com/bevyengine/bevy/pull/1070 -[1072]: https://github.com/bevyengine/bevy/pull/1072 -[1081]: https://github.com/bevyengine/bevy/pull/1081 -[1085]: https://github.com/bevyengine/bevy/pull/1085 -[1089]: https://github.com/bevyengine/bevy/pull/1089 -[1096]: https://github.com/bevyengine/bevy/pull/1096 -[1104]: https://github.com/bevyengine/bevy/pull/1104 -[1109]: https://github.com/bevyengine/bevy/pull/1109 -[1112]: https://github.com/bevyengine/bevy/pull/1112 -[1121]: https://github.com/bevyengine/bevy/pull/1121 -[1122]: https://github.com/bevyengine/bevy/pull/1122 -[1131]: https://github.com/bevyengine/bevy/pull/1131 -[1132]: https://github.com/bevyengine/bevy/pull/1132 -[1144]: https://github.com/bevyengine/bevy/pull/1144 -[1151]: https://github.com/bevyengine/bevy/pull/1151 -[1154]: https://github.com/bevyengine/bevy/pull/1154 -[1155]: https://github.com/bevyengine/bevy/pull/1155 -[1164]: https://github.com/bevyengine/bevy/pull/1164 -[1171]: https://github.com/bevyengine/bevy/pull/1171 -[1172]: https://github.com/bevyengine/bevy/pull/1172 -[1180]: https://github.com/bevyengine/bevy/pull/1180 -[1183]: https://github.com/bevyengine/bevy/pull/1183 -[1194]: https://github.com/bevyengine/bevy/pull/1194 -[1196]: https://github.com/bevyengine/bevy/pull/1196 -[1200]: https://github.com/bevyengine/bevy/pull/1200 -[1204]: https://github.com/bevyengine/bevy/pull/1204 -[1209]: https://github.com/bevyengine/bevy/pull/1209 -[1212]: https://github.com/bevyengine/bevy/pull/1212 -[1216]: https://github.com/bevyengine/bevy/pull/1216 -[1218]: https://github.com/bevyengine/bevy/pull/1218 -[1221]: https://github.com/bevyengine/bevy/pull/1221 -[1223]: https://github.com/bevyengine/bevy/pull/1223 -[1224]: https://github.com/bevyengine/bevy/pull/1224 -[1229]: https://github.com/bevyengine/bevy/pull/1229 -[1236]: https://github.com/bevyengine/bevy/pull/1236 -[1244]: https://github.com/bevyengine/bevy/pull/1244 -[1245]: https://github.com/bevyengine/bevy/pull/1245 -[1252]: https://github.com/bevyengine/bevy/pull/1252 -[1257]: https://github.com/bevyengine/bevy/pull/1257 -[1258]: https://github.com/bevyengine/bevy/pull/1258 -[1262]: https://github.com/bevyengine/bevy/pull/1262 -[1263]: https://github.com/bevyengine/bevy/pull/1263 -[1266]: https://github.com/bevyengine/bevy/pull/1266 -[1268]: https://github.com/bevyengine/bevy/pull/1268 -[1274]: https://github.com/bevyengine/bevy/pull/1274 -[1277]: https://github.com/bevyengine/bevy/pull/1277 -[1283]: https://github.com/bevyengine/bevy/pull/1283 -[1284]: https://github.com/bevyengine/bevy/pull/1284 -[1286]: https://github.com/bevyengine/bevy/pull/1286 -[1292]: https://github.com/bevyengine/bevy/pull/1292 -[1299]: https://github.com/bevyengine/bevy/pull/1299 -[1313]: https://github.com/bevyengine/bevy/pull/1313 -[1315]: https://github.com/bevyengine/bevy/pull/1315 -[1339]: https://github.com/bevyengine/bevy/pull/1339 -[1340]: https://github.com/bevyengine/bevy/pull/1340 -[1341]: https://github.com/bevyengine/bevy/pull/1341 -[1346]: https://github.com/bevyengine/bevy/pull/1346 -[1349]: https://github.com/bevyengine/bevy/pull/1349 -[1356]: https://github.com/bevyengine/bevy/pull/1356 -[1357]: https://github.com/bevyengine/bevy/pull/1357 -[1361]: https://github.com/bevyengine/bevy/pull/1361 -[1365]: https://github.com/bevyengine/bevy/pull/1365 -[1366]: https://github.com/bevyengine/bevy/pull/1366 -[1399]: https://github.com/bevyengine/bevy/pull/1399 -[1407]: https://github.com/bevyengine/bevy/pull/1407 -[1409]: https://github.com/bevyengine/bevy/pull/1409 -[1416]: https://github.com/bevyengine/bevy/pull/1416 -[1424]: https://github.com/bevyengine/bevy/pull/1424 -[1427]: https://github.com/bevyengine/bevy/pull/1427 -[1434]: https://github.com/bevyengine/bevy/pull/1434 -[1453]: https://github.com/bevyengine/bevy/pull/1453 -[1454]: https://github.com/bevyengine/bevy/pull/1454 -[1456]: https://github.com/bevyengine/bevy/pull/1456 -[1469]: https://github.com/bevyengine/bevy/pull/1469 -[1471]: https://github.com/bevyengine/bevy/pull/1471 -[1473]: https://github.com/bevyengine/bevy/pull/1473 -[1476]: https://github.com/bevyengine/bevy/pull/1476 -[1478]: https://github.com/bevyengine/bevy/pull/1478 -[1485]: https://github.com/bevyengine/bevy/pull/1485 -[1492]: https://github.com/bevyengine/bevy/pull/1492 -[1494]: https://github.com/bevyengine/bevy/pull/1494 -[1503]: https://github.com/bevyengine/bevy/pull/1503 -[1509]: https://github.com/bevyengine/bevy/pull/1509 -[1524]: https://github.com/bevyengine/bevy/pull/1524 -[1525]: https://github.com/bevyengine/bevy/pull/1525 -[1544]: https://github.com/bevyengine/bevy/pull/1544 -[1550]: https://github.com/bevyengine/bevy/pull/1550 -[1554]: https://github.com/bevyengine/bevy/pull/1554 -[1572]: https://github.com/bevyengine/bevy/pull/1572 -[1575]: https://github.com/bevyengine/bevy/pull/1575 -[1576]: https://github.com/bevyengine/bevy/pull/1576 -[1605]: https://github.com/bevyengine/bevy/pull/1605 -[1606]: https://github.com/bevyengine/bevy/pull/1606 -[1632]: https://github.com/bevyengine/bevy/pull/1632 -[1639]: https://github.com/bevyengine/bevy/pull/1639 -[1644]: https://github.com/bevyengine/bevy/pull/1644 -[1657]: https://github.com/bevyengine/bevy/pull/1657 -[1675]: https://github.com/bevyengine/bevy/pull/1675 -[1679]: https://github.com/bevyengine/bevy/pull/1679 -[1689]: https://github.com/bevyengine/bevy/pull/1689 -[1703]: https://github.com/bevyengine/bevy/pull/1703 -[1728]: https://github.com/bevyengine/bevy/pull/1728 -[1762]: https://github.com/bevyengine/bevy/pull/1762 - -## Version 0.4.0 (2020-12-19) - -### Added - -- [add bevymark benchmark example][273] -- [gltf: support camera and fix hierarchy][772] -- [Add tracing spans to schedules, stages, systems][789] -- [add example that represents contributors as bevy icons][801] -- [Add received character][805] -- [Add bevy_dylib to force dynamic linking of bevy][808] -- [Added RenderPass::set_scissor_rect][815] -- [`bevy_log`][836] - - Adds logging functionality as a Plugin. - - Changes internal logging to work with the new implementation. -- [cross-platform main function][847] -- [Controllable ambient light color][852] - - Added a resource to change the current ambient light color for PBR. -- [Added more basic color constants][859] -- [Add box shape][883] -- [Expose an EventId for events][894] -- [System Inputs, Outputs, and Chaining][876] -- [Expose an `EventId` for events][894] -- [Added `set_cursor_position` to `Window`][917] -- [Added new Bevy reflection system][926] - - Replaces the properties system -- [Add support for Apple Silicon][928] -- [Live reloading of shaders][937] -- [Store mouse cursor position in Window][940] -- [Add removal_detection example][945] -- [Additional vertex attribute value types][946] -- [Added WindowFocused event][956] -- [Tracing chrome span names][979] -- [Allow windows to be maximized][1004] -- [GLTF: load default material][1016] -- [can spawn a scene from a ChildBuilder, or directly set its parent when spawning it][1026] -- [add ability to load `.dds`, `.tga`, and `.jpeg` texture formats][1038] -- [add ability to provide custom a `AssetIo` implementation][1037] - -### Changed - -- [delegate layout reflection to RenderResourceContext][691] -- [Fall back to remove components one by one when failing to remove a bundle][719] -- [Port hecs derive macro improvements][761] -- [Use glyph_brush_layout and add text alignment support][765] -- [upgrade glam and hexasphere][791] -- [Flexible ECS Params][798] -- [Make Timer.tick return &Self][820] -- [FileAssetIo includes full path on error][821] -- [Removed ECS query APIs that could easily violate safety from the public interface][829] -- [Changed Query filter API to be easier to understand][834] -- [bevy_render: delegate buffer aligning to render_resource_context][842] -- [wasm32: non-spirv shader specialization][843] -- [Renamed XComponents to XBundle][863] -- [Check for conflicting system resource parameters][864] -- [Tweaks to TextureAtlasBuilder.finish()][887] -- [do not spend time drawing text with is_visible = false][893] -- [Extend the Texture asset type to support 3D data][903] -- [Breaking changes to timer API][914] - - Created getters and setters rather than exposing struct members. -- [Removed timer auto-ticking system][931] - - Added an example of how to tick timers manually. -- [When a task scope produces <= 1 task to run, run it on the calling thread immediately][932] -- [Breaking changes to Time API][934] - - Created getters to get `Time` state and made members private. - - Modifying `Time`'s values directly is no longer possible outside of bevy. -- [Use `mailbox` instead of `fifo` for vsync on supported systems][920] -- [switch winit size to logical to be dpi independent][947] -- [Change bevy_input::Touch API to match similar APIs][952] -- [Run parent-update and transform-propagation during the "post-startup" stage (instead of "startup")][955] -- [Renderer Optimization Round 1][958] -- [Change`TextureAtlasBuilder` into expected Builder conventions][969] -- [Optimize Text rendering / SharedBuffers][972] -- [hidpi swap chains][973] -- [optimize asset gpu data transfer][987] -- [naming coherence for cameras][995] -- [Schedule v2][1021] -- [Use shaderc for aarch64-apple-darwin][1027] -- [update `Window`'s `width` & `height` methods to return `f32`][1033] -- [Break out Visible component from Draw][1034] - - Users setting `Draw::is_visible` or `Draw::is_transparent` should now set `Visible::is_visible` and `Visible::is_transparent` -- [`winit` upgraded from version 0.23 to version 0.24][1043] -- [set is_transparent to true by default for UI bundles][1071] - -### Fixed - -- [Fixed typos in KeyCode identifiers][857] -- [Remove redundant texture copies in TextureCopyNode][871] -- [Fix a deadlock that can occur when using scope() on ComputeTaskPool from within a system][892] -- [Don't draw text that isn't visible][893] -- [Use `instant::Instant` for WASM compatibility][895] -- [Fix pixel format conversion in bevy_gltf][897] -- [Fixed duplicated children when spawning a Scene][904] -- [Corrected behaviour of the UI depth system][905] -- [Allow despawning of hierarchies in threadlocal systems][908] -- [Fix `RenderResources` index slicing][948] -- [Run parent-update and transform-propagation during the "post-startup" stage][955] -- [Fix collision detection by comparing abs() penetration depth][966] -- [deal with rounding issue when creating the swap chain][997] -- [only update components for entities in map][1023] -- [Don't panic when attempting to set shader defs from an asset that hasn't loaded yet][1035] - -[273]: https://github.com/bevyengine/bevy/pull/273 -[691]: https://github.com/bevyengine/bevy/pull/691 -[719]: https://github.com/bevyengine/bevy/pull/719 -[761]: https://github.com/bevyengine/bevy/pull/761 -[765]: https://github.com/bevyengine/bevy/pull/765 -[772]: https://github.com/bevyengine/bevy/pull/772 -[789]: https://github.com/bevyengine/bevy/pull/789 -[791]: https://github.com/bevyengine/bevy/pull/791 -[798]: https://github.com/bevyengine/bevy/pull/798 -[801]: https://github.com/bevyengine/bevy/pull/801 -[805]: https://github.com/bevyengine/bevy/pull/805 -[808]: https://github.com/bevyengine/bevy/pull/808 -[815]: https://github.com/bevyengine/bevy/pull/815 -[820]: https://github.com/bevyengine/bevy/pull/820 -[821]: https://github.com/bevyengine/bevy/pull/821 -[829]: https://github.com/bevyengine/bevy/pull/829 -[834]: https://github.com/bevyengine/bevy/pull/834 -[836]: https://github.com/bevyengine/bevy/pull/836 -[842]: https://github.com/bevyengine/bevy/pull/842 -[843]: https://github.com/bevyengine/bevy/pull/843 -[847]: https://github.com/bevyengine/bevy/pull/847 -[852]: https://github.com/bevyengine/bevy/pull/852 -[857]: https://github.com/bevyengine/bevy/pull/857 -[859]: https://github.com/bevyengine/bevy/pull/859 -[863]: https://github.com/bevyengine/bevy/pull/863 -[864]: https://github.com/bevyengine/bevy/pull/864 -[871]: https://github.com/bevyengine/bevy/pull/871 -[876]: https://github.com/bevyengine/bevy/pull/876 -[883]: https://github.com/bevyengine/bevy/pull/883 -[887]: https://github.com/bevyengine/bevy/pull/887 -[892]: https://github.com/bevyengine/bevy/pull/892 -[893]: https://github.com/bevyengine/bevy/pull/893 -[894]: https://github.com/bevyengine/bevy/pull/894 -[895]: https://github.com/bevyengine/bevy/pull/895 -[897]: https://github.com/bevyengine/bevy/pull/897 -[903]: https://github.com/bevyengine/bevy/pull/903 -[904]: https://github.com/bevyengine/bevy/pull/904 -[905]: https://github.com/bevyengine/bevy/pull/905 -[908]: https://github.com/bevyengine/bevy/pull/908 -[914]: https://github.com/bevyengine/bevy/pull/914 -[917]: https://github.com/bevyengine/bevy/pull/917 -[920]: https://github.com/bevyengine/bevy/pull/920 -[926]: https://github.com/bevyengine/bevy/pull/926 -[928]: https://github.com/bevyengine/bevy/pull/928 -[931]: https://github.com/bevyengine/bevy/pull/931 -[932]: https://github.com/bevyengine/bevy/pull/932 -[934]: https://github.com/bevyengine/bevy/pull/934 -[937]: https://github.com/bevyengine/bevy/pull/937 -[940]: https://github.com/bevyengine/bevy/pull/940 -[945]: https://github.com/bevyengine/bevy/pull/945 -[946]: https://github.com/bevyengine/bevy/pull/946 -[947]: https://github.com/bevyengine/bevy/pull/947 -[948]: https://github.com/bevyengine/bevy/pull/948 -[952]: https://github.com/bevyengine/bevy/pull/952 -[955]: https://github.com/bevyengine/bevy/pull/955 -[956]: https://github.com/bevyengine/bevy/pull/956 -[958]: https://github.com/bevyengine/bevy/pull/958 -[966]: https://github.com/bevyengine/bevy/pull/966 -[969]: https://github.com/bevyengine/bevy/pull/969 -[972]: https://github.com/bevyengine/bevy/pull/972 -[973]: https://github.com/bevyengine/bevy/pull/973 -[979]: https://github.com/bevyengine/bevy/pull/979 -[987]: https://github.com/bevyengine/bevy/pull/987 -[995]: https://github.com/bevyengine/bevy/pull/995 -[997]: https://github.com/bevyengine/bevy/pull/997 -[1004]: https://github.com/bevyengine/bevy/pull/1004 -[1016]: https://github.com/bevyengine/bevy/pull/1016 -[1021]: https://github.com/bevyengine/bevy/pull/1021 -[1023]: https://github.com/bevyengine/bevy/pull/1023 -[1026]: https://github.com/bevyengine/bevy/pull/1026 -[1027]: https://github.com/bevyengine/bevy/pull/1027 -[1033]: https://github.com/bevyengine/bevy/pull/1033 -[1034]: https://github.com/bevyengine/bevy/pull/1034 -[1035]: https://github.com/bevyengine/bevy/pull/1035 -[1037]: https://github.com/bevyengine/bevy/pull/1037 -[1038]: https://github.com/bevyengine/bevy/pull/1038 -[1043]: https://github.com/bevyengine/bevy/pull/1043 -[1071]: https://github.com/bevyengine/bevy/pull/1071 - -## Version 0.3.0 (2020-11-03) - -### Added - -- [Touch Input][696] -- [iOS XCode Project][539] -- [Android Example and use bevy-glsl-to-spirv 0.2.0][740] -- [Introduce Mouse capture API][679] -- [`bevy_input::touch`: implement touch input][696] -- [D-pad support on MacOS][653] -- [Support for Android file system][723] -- [app: PluginGroups and DefaultPlugins][744] - - `PluginGroup` is a collection of plugins where each plugin can be enabled or disabled. -- [Support to get gamepad button/trigger values using `Axis`][683] -- [Expose Winit decorations][627] -- [Enable changing window settings at runtime][644] -- [Expose a pointer of EventLoopProxy to process custom messages][674] -- [Add a way to specify padding/ margins between sprites in a TextureAtlas][460] -- [Add `bevy_ecs::Commands::remove` for bundles][579] -- [impl `Default` for `TextureFormat`][675] -- [Expose current_entity in ChildBuilder][595] -- [`AppBuilder::add_thread_local_resource`][671] -- [`Commands::write_world_boxed` takes a pre-boxed world writer to the ECS's command queue][661] -- [`FrameTimeDiagnosticsPlugin` now shows "frame count" in addition to "frame time" and "fps"][678] -- [Add hierarchy example][565] -- [`WgpuPowerOptions` for choosing between low power, high performance, and adaptive power][397] -- Derive `Debug` for more types: [#597][597], [#632][632] -- Index buffer specialization - - [Allows the use of U32 indices in Mesh index buffers in addition to the usual U16 indices][568] - - [Switch to u32 indices by default][572] -- More instructions for system dependencies - - [Add `systemd-devel` for Fedora Linux dependencies][528] - - [Add `libudev-dev` to Ubuntu dependencies][538] - - [Add Void Linux to linux dependencies file][645] - - [WSL2 instructions][727] -- [Suggest `-Zrun-dsymutil-no` for faster compilation on MacOS][552] - -### Changed - -- [ecs: ergonomic query.iter(), remove locks, add QuerySets][741] - - `query.iter()` is now a real iterator! - - `QuerySet` allows working with conflicting queries and is checked at compile-time. -- [Rename `query.entity()` and `query.get()`][752] - - `query.get::(entity)` is now `query.get_component::(entity)` - - `query.entity(entity)` is now `query.get(entity)` -- [Asset system rework and GLTF scene loading][693] -- [Introduces WASM implementation of `AssetIo`][703] -- [Move transform data out of Mat4][596] -- [Separate gamepad state code from gamepad event code and other customizations][700] -- [gamepad: expose raw and filtered gamepad events][711] -- [Do not depend on `spirv-reflect` on `wasm32` target][689] -- [Move dynamic plugin loading to its own optional crate][544] -- [Add field to `WindowDescriptor` on wasm32 targets to optionally provide an existing canvas element as winit window][515] -- [Adjust how `ArchetypeAccess` tracks mutable & immutable deps][660] -- [Use `FnOnce` in `Commands` and `ChildBuilder` where possible][535] -- [Runners explicitly call `App.initialize()`][690] -- [sRGB awareness for `Color`][616] - - Color is now assumed to be provided in the non-linear sRGB colorspace. - Constructors such as `Color::rgb` and `Color::rgba` will be converted to linear sRGB. - - New methods `Color::rgb_linear` and `Color::rgba_linear` will accept colors already in linear sRGB (the old behavior) - - Individual color-components must now be accessed through setters and getters. -- [`Mesh` overhaul with custom vertex attributes][599] - - Any vertex attribute can now be added over `mesh.attributes.insert()`. - - See `example/shader/mesh_custom_attribute.rs` - - Removed `VertexAttribute`, `Vertex`, `AsVertexBufferDescriptor`. - - For missing attributes (requested by shader, but not defined by mesh), Bevy will provide a zero-filled fallback buffer. -- Despawning an entity multiple times causes a debug-level log message to be emitted instead of a panic: [#649][649], [#651][651] -- [Migrated to Rodio 0.12][692] - - New method of playing audio can be found in the examples. -- Added support for inserting custom initial values for `Local` system resources [#745][745] - -### Fixed - -- [Properly update bind group ids when setting dynamic bindings][560] -- [Properly exit the app on AppExit event][610] -- [Fix FloatOrd hash being different for different NaN values][618] -- [Fix Added behavior for QueryOne get][543] -- [Update camera_system to fix issue with late camera addition][488] -- [Register `IndexFormat` as a property][664] -- [Fix breakout example bug][685] -- [Fix PreviousParent lag by merging parent update systems][713] -- [Fix bug of connection event of gamepad at startup][730] -- [Fix wavy text][725] - -[397]: https://github.com/bevyengine/bevy/pull/397 -[460]: https://github.com/bevyengine/bevy/pull/460 -[488]: https://github.com/bevyengine/bevy/pull/488 -[515]: https://github.com/bevyengine/bevy/pull/515 -[528]: https://github.com/bevyengine/bevy/pull/528 -[535]: https://github.com/bevyengine/bevy/pull/535 -[538]: https://github.com/bevyengine/bevy/pull/538 -[539]: https://github.com/bevyengine/bevy/pull/539 -[543]: https://github.com/bevyengine/bevy/pull/543 -[544]: https://github.com/bevyengine/bevy/pull/544 -[552]: https://github.com/bevyengine/bevy/pull/552 -[560]: https://github.com/bevyengine/bevy/pull/560 -[565]: https://github.com/bevyengine/bevy/pull/565 -[568]: https://github.com/bevyengine/bevy/pull/568 -[572]: https://github.com/bevyengine/bevy/pull/572 -[579]: https://github.com/bevyengine/bevy/pull/579 -[595]: https://github.com/bevyengine/bevy/pull/595 -[596]: https://github.com/bevyengine/bevy/pull/596 -[597]: https://github.com/bevyengine/bevy/pull/597 -[599]: https://github.com/bevyengine/bevy/pull/599 -[610]: https://github.com/bevyengine/bevy/pull/610 -[616]: https://github.com/bevyengine/bevy/pull/616 -[618]: https://github.com/bevyengine/bevy/pull/618 -[627]: https://github.com/bevyengine/bevy/pull/627 -[632]: https://github.com/bevyengine/bevy/pull/632 -[644]: https://github.com/bevyengine/bevy/pull/644 -[645]: https://github.com/bevyengine/bevy/pull/645 -[649]: https://github.com/bevyengine/bevy/pull/649 -[651]: https://github.com/bevyengine/bevy/pull/651 -[653]: https://github.com/bevyengine/bevy/pull/653 -[660]: https://github.com/bevyengine/bevy/pull/660 -[661]: https://github.com/bevyengine/bevy/pull/661 -[664]: https://github.com/bevyengine/bevy/pull/664 -[671]: https://github.com/bevyengine/bevy/pull/671 -[674]: https://github.com/bevyengine/bevy/pull/674 -[675]: https://github.com/bevyengine/bevy/pull/675 -[678]: https://github.com/bevyengine/bevy/pull/678 -[679]: https://github.com/bevyengine/bevy/pull/679 -[683]: https://github.com/bevyengine/bevy/pull/683 -[685]: https://github.com/bevyengine/bevy/pull/685 -[689]: https://github.com/bevyengine/bevy/pull/689 -[690]: https://github.com/bevyengine/bevy/pull/690 -[692]: https://github.com/bevyengine/bevy/pull/692 -[693]: https://github.com/bevyengine/bevy/pull/693 -[696]: https://github.com/bevyengine/bevy/pull/696 -[700]: https://github.com/bevyengine/bevy/pull/700 -[703]: https://github.com/bevyengine/bevy/pull/703 -[711]: https://github.com/bevyengine/bevy/pull/711 -[713]: https://github.com/bevyengine/bevy/pull/713 -[723]: https://github.com/bevyengine/bevy/pull/723 -[725]: https://github.com/bevyengine/bevy/pull/725 -[727]: https://github.com/bevyengine/bevy/pull/727 -[730]: https://github.com/bevyengine/bevy/pull/730 -[740]: https://github.com/bevyengine/bevy/pull/740 -[741]: https://github.com/bevyengine/bevy/pull/741 -[744]: https://github.com/bevyengine/bevy/pull/744 -[745]: https://github.com/bevyengine/bevy/pull/745 -[752]: https://github.com/bevyengine/bevy/pull/752 - -## Version 0.2.1 (2020-9-20) - -### Fixed - -- [Remove UI queue print][521] -- [Use async executor 1.3.0][526] - -[521]: https://github.com/bevyengine/bevy/pull/521 -[526]: https://github.com/bevyengine/bevy/pull/526 - -## Version 0.2.0 (2020-9-19) - -### Added - -- [Task System for Bevy][384] - - Replaces rayon with a custom designed task system that consists of several "TaskPools". - - Exports `IOTaskPool`, `ComputePool`, and `AsyncComputePool` in `bevy_tasks` crate. -- [Parallel queries for distributing work over with the `ParallelIterator` trait.][292] - - e.g. `query.iter().par_iter(batch_size).for_each(/* ... */)` -- [Added gamepad support using Gilrs][280] -- [Implement WASM support for bevy_winit][503] -- [Create winit canvas under WebAssembly][506] -- [Implement single threaded task scheduler for WebAssembly][496] -- [Support for binary glTF (.glb).][271] -- [Support for `Or` in ECS queries.][358] -- [Added methods `unload()` and `unload_sync()` on `SceneSpawner` for unloading scenes.][339]. -- [Custom rodio source for audio.][145] - - `AudioOuput` is now able to play anything `Decodable`. -- [`Color::hex`][362] for creating `Color` from string hex values. - - Accepts the forms RGB, RGBA, RRGGBB, and RRGGBBAA. -- [`Color::rgb_u8` and `Color::rgba_u8`.][381] -- [Added `bevy_render::pass::ClearColor` to prelude.][396] -- [`SpriteResizeMode` may choose how `Sprite` resizing should be handled. `Automatic` by default.][430] -- [Added methods on `Input`][428] for iterator access to keys. - - `get_pressed()`, `get_just_pressed()`, `get_just_released()` -- [Derived `Copy` for `MouseScrollUnit`.][270] -- [Derived `Clone` for UI component bundles.][390] -- [Some examples of documentation][338] -- [Update docs for Updated, Changed and Mutated][451] -- Tips for faster builds on macOS: [#312][312], [#314][314], [#433][433] -- Added and documented cargo features - - [Created document `docs/cargo_features.md`.][249] - - [Added features for x11 and wayland display servers.][249] - - [and added a feature to disable libloading.][363] (helpful for WASM support) -- Added more instructions for Linux dependencies - - [Arch / Manjaro][275], [NixOS][290], [Ubuntu][463] and [Solus][331] -- [Provide shell.nix for easier compiling with nix-shell][491] -- [Add `AppBuilder::add_startup_stage_|before/after`][505] - -### Changed - -- [Transform rewrite][374] -- [Use generational entity ids and other optimizations][504] -- [Optimize transform systems to only run on changes.][417] -- [Send an AssetEvent when modifying using `get_id_mut`][323] -- [Rename `Assets::get_id_mut` -> `Assets::get_with_id_mut`][332] -- [Support multiline text in `DrawableText`][183] -- [iOS: use shaderc-rs for glsl to spirv compilation][324] -- [Changed the default node size to Auto instead of Undefined to match the Stretch implementation.][304] -- [Load assets from root path when loading directly][478] -- [Add `render` feature][485], which makes the entire render pipeline optional. - -### Fixed - -- [Properly track added and removed RenderResources in RenderResourcesNode.][361] - - Fixes issues where entities vanished or changed color when new entities were spawned/despawned. -- [Fixed sprite clipping at same depth][385] - - Transparent sprites should no longer clip. -- [Check asset path existence][345] -- [Fixed deadlock in hot asset reloading][376] -- [Fixed hot asset reloading on Windows][394] -- [Allow glTFs to be loaded that don't have uvs and normals][406] -- [Fixed archetypes_generation being incorrectly updated for systems][383] -- [Remove child from parent when it is despawned][386] -- [Initialize App.schedule systems when running the app][444] -- [Fix missing asset info path for synchronous loading][486] -- [fix font atlas overflow][495] -- [do not assume font handle is present in assets][490] - -### Internal Improvements - -- Many improvements to Bevy's CI [#325][325], [#349][349], [#357][357], [#373][373], [#423][423] - -[145]: https://github.com/bevyengine/bevy/pull/145 -[183]: https://github.com/bevyengine/bevy/pull/183 -[249]: https://github.com/bevyengine/bevy/pull/249 -[270]: https://github.com/bevyengine/bevy/pull/270 -[271]: https://github.com/bevyengine/bevy/pull/271 -[275]: https://github.com/bevyengine/bevy/pull/275 -[280]: https://github.com/bevyengine/bevy/pull/280 -[290]: https://github.com/bevyengine/bevy/pull/290 -[292]: https://github.com/bevyengine/bevy/pull/292 -[304]: https://github.com/bevyengine/bevy/pull/304 -[312]: https://github.com/bevyengine/bevy/pull/312 -[314]: https://github.com/bevyengine/bevy/pull/314 -[323]: https://github.com/bevyengine/bevy/pull/323 -[324]: https://github.com/bevyengine/bevy/pull/324 -[325]: https://github.com/bevyengine/bevy/pull/325 -[331]: https://github.com/bevyengine/bevy/pull/331 -[332]: https://github.com/bevyengine/bevy/pull/332 -[338]: https://github.com/bevyengine/bevy/pull/332 -[345]: https://github.com/bevyengine/bevy/pull/345 -[349]: https://github.com/bevyengine/bevy/pull/349 -[357]: https://github.com/bevyengine/bevy/pull/357 -[358]: https://github.com/bevyengine/bevy/pull/358 -[361]: https://github.com/bevyengine/bevy/pull/361 -[362]: https://github.com/bevyengine/bevy/pull/362 -[363]: https://github.com/bevyengine/bevy/pull/363 -[373]: https://github.com/bevyengine/bevy/pull/373 -[374]: https://github.com/bevyengine/bevy/pull/374 -[376]: https://github.com/bevyengine/bevy/pull/376 -[381]: https://github.com/bevyengine/bevy/pull/381 -[383]: https://github.com/bevyengine/bevy/pull/383 -[384]: https://github.com/bevyengine/bevy/pull/384 -[385]: https://github.com/bevyengine/bevy/pull/385 -[386]: https://github.com/bevyengine/bevy/pull/386 -[390]: https://github.com/bevyengine/bevy/pull/390 -[394]: https://github.com/bevyengine/bevy/pull/394 -[396]: https://github.com/bevyengine/bevy/pull/396 -[339]: https://github.com/bevyengine/bevy/pull/339 -[406]: https://github.com/bevyengine/bevy/pull/406 -[417]: https://github.com/bevyengine/bevy/pull/417 -[423]: https://github.com/bevyengine/bevy/pull/423 -[428]: https://github.com/bevyengine/bevy/pull/428 -[430]: https://github.com/bevyengine/bevy/pull/430 -[433]: https://github.com/bevyengine/bevy/pull/433 -[444]: https://github.com/bevyengine/bevy/pull/444 -[451]: https://github.com/bevyengine/bevy/pull/451 -[463]: https://github.com/bevyengine/bevy/pull/463 -[478]: https://github.com/bevyengine/bevy/pull/478 -[485]: https://github.com/bevyengine/bevy/pull/485 -[486]: https://github.com/bevyengine/bevy/pull/486 -[490]: https://github.com/bevyengine/bevy/pull/490 -[491]: https://github.com/bevyengine/bevy/pull/491 -[495]: https://github.com/bevyengine/bevy/pull/495 -[496]: https://github.com/bevyengine/bevy/pull/496 -[503]: https://github.com/bevyengine/bevy/pull/503 -[504]: https://github.com/bevyengine/bevy/pull/504 -[505]: https://github.com/bevyengine/bevy/pull/505 -[506]: https://github.com/bevyengine/bevy/pull/506 - -## Version 0.1.3 (2020-8-22) - -## Version 0.1.2 (2020-8-10) - -## Version 0.1.1 (2020-8-10) - -## Version 0.1.0 (2020-8-10) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92ea2d0b6f2724..af24766cf184e7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,466 +1,3 @@ # Contributing to Bevy -Hey, so you're interested in contributing to Bevy! -Feel free to pitch in on whatever interests you and we'll be happy to help you contribute. - -Check out our community's [Code of Conduct](https://github.com/bevyengine/bevy/blob/main/CODE_OF_CONDUCT.md) and feel free to say hi on [Discord] if you'd like. -It's a nice place to chat about Bevy development, ask questions, and get to know the other contributors and users in a less formal setting. - -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. - -We're thrilled to have you along as we build! - -## Getting oriented - -Bevy, like any general-purpose game engine, is a large project! -It can be a bit overwhelming to start, so here's the bird's-eye view. - -The [Bevy Engine Organization](https://github.com/bevyengine) has 4 primary repos: - -1. [**`bevy`**](https://github.com/bevyengine/bevy): This is where the engine itself lives. The bulk of development work occurs here. -2. [**`bevy-website`**](https://github.com/bevyengine/bevy-website): Where the [official website](https://bevyengine.org/), release notes, Bevy Book, and Bevy Assets are hosted. It is created using the Zola static site generator. -3. [**`bevy-assets`**](https://github.com/bevyengine/bevy-assets): A collection of community-made tutorials, plugins, crates, games, and tools! Make a PR if you want to showcase your projects there! -4. [**`rfcs`**](https://github.com/bevyengine/rfcs): A place to collaboratively build and reach consensus on designs for large or controversial features. - -The `bevy` repo itself contains many smaller subcrates. Most of them can be used by themselves and many of them can be modularly replaced. This enables developers to pick and choose the parts of Bevy that they want to use. - -Some crates of interest: - -* [**`bevy_ecs`**](./crates/bevy_ecs): The core data model for Bevy. Most Bevy features are implemented on top of it. It is also fully functional as a stand-alone ECS, which can be very valuable if you're looking to integrate it with other game engines or use it for non-game executables. -* [**`bevy_app`**](./crates/bevy_app): The api used to define Bevy Plugins and compose them together into Bevy Apps. -* [**`bevy_tasks`**](./crates/bevy_tasks): Our light-weight async executor. This drives most async and parallel code in Bevy. -* [**`bevy_render`**](./crates/bevy_render): Our core renderer API. It handles interaction with the GPU, such as the creation of Meshes, Textures, and Shaders. It also exposes a modular Render Graph for composing render pipelines. All 2D and 3D render features are implemented on top of this crate. - -## What we're trying to build - -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. - -Bevy also currently has the following "development process" goals: - -* **Rapid experimentation over API stability**: We need the freedom to experiment and iterate in order to build the best engine we can. This will change over time as APIs prove their staying power. -* **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. -* **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. -* **Don't re-invent every wheel**: As a counter to the previous point, don't re-invent everything at all costs. If there is a crate in the Rust ecosystem that is the "de-facto" standard (ex: wgpu, winit, cpal), we should heavily consider using it. Bevy should be a positive force in the ecosystem. We should drive the improvements we need into these core ecosystem crates. -* **Rust-first**: Engine and user-facing code should optimize and encourage Rust-only workflows. Adding additional languages increases internal complexity, fractures the Bevy ecosystem, and makes it harder for users to understand the engine. Never compromise a Rust interface in the interest of compatibility with other languages. -* **Thoughtful public interfaces over maximal configurability**: Symbols and apis should be private by default. Every public API should be thoughtfully and consistently designed. Don't expose unnecessary internal implementation details. Don't allow users to "shoot themselves in the foot". Favor one "happy path" api over multiple apis for different use cases. -* **Welcome new contributors**: Invest in new contributors. Help them fill knowledge and skill gaps. Don't ever gatekeep Bevy development according to notions of required skills or credentials. Help new developers find their niche. -* **Civil discourse**: We need to collectively discuss ideas and the best ideas *should* win. But conversations need to remain respectful at all times. Remember that we're all in this together. Always follow our [Code of Conduct](https://github.com/bevyengine/bevy/blob/main/CODE_OF_CONDUCT.md). -* **Test what you need to**: Write useful tests. Don't write tests that aren't useful. We *generally* aren't strict about unit testing every line of code. We don't want you to waste your time. But at the same time: - * Most new features should have at least one minimal [example](https://github.com/bevyengine/bevy/tree/main/examples). These also serve as simple integration tests, as they are run as part of our CI process. - * The more complex or "core" a feature is, the more strict we are about unit tests. Use your best judgement here. We will let you know if your pull request needs more tests. We use [Rust's built in testing framework](https://doc.rust-lang.org/book/ch11-01-writing-tests.html). - -## The Bevy Organization - -The Bevy Organization is the group of people responsible for stewarding the Bevy project. It handles things like merging pull requests, choosing project direction, managing bugs / issues / feature requests, running the Bevy website, controlling access to secrets, defining and enforcing best practices, etc. - -Note that you *do not* need to be a member of the Bevy Organization to contribute to Bevy. Community contributors (this means you) can freely open issues, submit pull requests, and review pull requests. - -Check out our dedicated [Bevy Organization document](/docs/the_bevy_organization.md) to learn more about how we're organized. - -### Classifying PRs - -[Labels](https://github.com/bevyengine/bevy/labels) are our primary tool to organize work. -Each label has a prefix denoting its category: - -* **D:** Difficulty. In order, these are: - * `D-Trivial`: typos, obviously incorrect one-line bug fixes, code reorganization, renames - * `D-Straightforward`: simple bug fixes and API improvements, docs, test and examples - * `D-Modest`: new features, refactors, challenging bug fixes - * `D-Complex`: rewrites and unusually complex features - * When applied to an issue, these labels reflect the estimated level of expertise (not time) required to fix the issue. - * When applied to a PR, these labels reflect the estimated level of expertise required to *review* the PR. - * The `D-Domain-Expert` and `D-Domain-Agnostic` labels are modifiers, which describe if unusually high or low degrees of domain-specific knowledge are required. - * The `D-Unsafe` label is applied to any code that touches `unsafe` Rust, which requires special skills and scrutiny. -* **X:** Controversiality. In order, these are: - * `X-Uncontroversial`: everyone should agree that this is a good idea - * `X-Contentious`: there's real design thought needed to ensure that this is the right path forward - * `X-Controversial`: there's active disagreement and/or large-scale architectural implications involved - * `X-Blessed`: work that was controversial, but whose controversial (but perhaps not technical) elements have been endorsed by the relevant decision makers. -* **A:** Area (e.g. A-Animation, A-ECS, A-Rendering, ...). -* **C:** Category (e.g. C-Breaking-Change, C-Code-Quality, C-Docs, ...). -* **O:** Operating System (e.g. O-Linux, O-Web, O-Windows, ...). -* **P:** Priority (e.g. P-Critical, P-High, ...) - * Most work is not explicitly categorized by priority: volunteer work mostly occurs on an ad hoc basis depending on contributor interests -* **S:** Status (e.g. S-Blocked, S-Needs-Review, S-Needs-Design, ...). - -The rules for how PRs get merged depend on their classification by controversy and difficulty. -More difficult PRs will require more careful review from experts, -while more controversial PRs will require rewrites to reduce the costs involved and/or sign-off from Subject Matter Experts and Maintainers. - -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 them to be reviewed together. - -Some things that are reason to apply the [`S-Controversial`] label to a PR: - -1. Changes to a project-wide workflow or style. -2. New architecture for a large feature. -3. Serious tradeoffs were made. -4. Heavy user impact. -5. New ways for users to make mistakes (footguns). -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). - -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). -2. High levels of technical complexity. -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). - -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). - -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). - -Examples of PRs that are both [`S-Controversial`] and [`D-Complex`]: - -* [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). - -[`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 - -### Prioritizing PRs and issues - -We use [Milestones](https://github.com/bevyengine/bevy/milestones) to track issues and PRs that: - -* Need to be merged/fixed before the next release. This is generally for extremely bad bugs i.e. UB or important functionality being broken. -* Would have higher user impact and are almost ready to be merged/fixed. - -There are also two priority labels: [`P-Critical`](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AP-Critical) and [`P-High`](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AP-High) that can be used to find issues and PRs that need to be resolved urgently. - -### Closing PRs and Issues - -From time to time, PRs are unsuitable to be merged in a way that cannot be readily fixed. -Rather than leaving these PRs open in limbo indefinitely, they should simply be closed. - -This might happen if: - -1. The PR is spam or malicious. -2. The work has already been done elsewhere or is otherwise fully obsolete. -3. The PR was successfully adopted. -4. The work is particularly low quality, and the author is resistant to coaching. -5. The work adds features or abstraction of limited value, especially in a way that could easily be recreated outside of the engine. -6. The work has been sitting in review for so long and accumulated so many conflicts that it would be simpler to redo it from scratch. -7. The PR is pointlessly large, and should be broken into multiple smaller PRs for easier review. - -PRs that are `S-Adopt-Me` should be left open, but only if they're genuinely more useful to rebase rather than simply use as a reference. - -There are several paths for PRs to be closed: - -1. Obviously, authors may close their own PRs for any reason at any time. -2. If a PR is clearly spam or malicious, anyone with triage rights is encouraged to close out the PR and report it to Github. -3. If the work has already been done elsewhere, adopted or otherwise obsoleted, anyone with triage rights is encouraged to close out the PR with an explanatory comment. -4. Anyone may nominate a PR for closure, by bringing it to the attention of the author and / or one of the SMEs / maintainers. Let them press the button, but this is generally well-received and helpful. -5. SMEs or maintainers may and are encouraged to unilaterally close PRs that fall into one or more of the remaining categories. -6. In the case of PRs where some members of the community (other than the author) are in favor and some are opposed, any two relevant SMEs or maintainers may act in concert to close the PR. - -When closing a PR, check if it has an issue linked. -If it does not, you should strongly consider creating an issue and linking the now-closed PR to help make sure the previous work can be discovered and credited. - -## Making changes to Bevy - -Most changes don't require much "process". If your change is relatively straightforward, just do the following: - -1. A community member (that's you!) creates one of the following: - * [GitHub Discussions]: An informal discussion with the community. This is the place to start if you want to propose a feature or specific implementation. - * [Issue](https://github.com/bevyengine/bevy/issues): A formal way for us to track a bug or feature. Please look for duplicates before opening a new issue and consider starting with a Discussion. - * [Pull Request](https://github.com/bevyengine/bevy/pulls) (or PR for short): A request to merge code changes. This starts our "review process". You are welcome to start with a pull request, but consider starting with an Issue or Discussion for larger changes (or if you aren't certain about a design). We don't want anyone to waste their time on code that didn't have a chance to be merged! But conversely, sometimes PRs are the most efficient way to propose a change. Just use your own judgement here. -2. Other community members review and comment in an ad-hoc fashion. Active subject matter experts may be pulled into a thread using `@mentions`. If your PR has been quiet for a while and is ready for review, feel free to leave a message to "bump" the thread, or bring it up on [Discord](https://discord.gg/bevy) in an appropriate engine development channel. -3. Once they're content with the pull request (design, code quality, documentation, tests), individual reviewers leave "Approved" reviews. -4. After consensus has been reached (typically two approvals from the community or one for extremely simple changes) and CI passes, the [S-Ready-For-Final-Review](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AS-Ready-For-Final-Review) label is added. -5. When they find time, someone with merge rights performs a final code review and queue the PR for merging. - -### Complex changes - -Individual contributors often lead major new features and reworks. However these changes require more design work and scrutiny. Complex changes like this tend to go through the following lifecycle: - -1. A need or opportunity is identified and an issue is made, laying out the general problem. -2. As needed, this is discussed further on that issue thread, in cross-linked [GitHub Discussion] threads, or on [Discord] in the Engine Development channels. -3. Either a Draft Pull Request or an RFC is made. As discussed in the [RFC repo](https://github.com/bevyengine/rfcs), complex features need RFCs, but these can be submitted before or after prototyping work has been started. -4. If feasible, parts that work on their own (even if they're only useful once the full complex change is merged) get split out into individual PRs to make them easier to review. -5. The community as a whole helps improve the Draft PR and/or RFC, leaving comments, making suggestions, and submitting pull requests to the original branch. -6. Once the RFC is merged and/or the Draft Pull Request is transitioned out of draft mode, the [normal change process outlined in the previous section](#making-changes-to-bevy) can begin. - -## How you can help - -If you've made it to this page, you're probably already convinced that Bevy is a project you'd like to see thrive. -But how can *you* help? - -No matter your experience level with Bevy or Rust or your level of commitment, there are ways to meaningfully contribute. -Take a look at the sections that follow to pick a route (or five) that appeal to you. - -If you ever find yourself at a loss for what to do, or in need of mentorship or advice on how to contribute to Bevy, feel free to ask in [Discord] and one of our more experienced community members will be happy to help. - -### Join a working group - -Active initiatives in Bevy are organized into temporary working groups: choosing one of those and asking how to help can be a fantastic way to get up to speed and be immediately useful. - -Working groups are public, open-membership groups that work together to tackle a broad-but-scoped initiative. -The work that they do is coordinated in a forum-channel on [Discord](https://discord.gg/bevy), although they also create issues and may use project boards for tangible work that needs to be done. - -There are no special requirements to be a member, and no formal membership list or leadership. -Anyone can help, and you should expect to compromise and work together with others to bring a shared vision to life. -Working groups are *spaces*, not clubs. - -### Start a working group - -When tackling a complex initiative, friends and allies can make things go much more smoothly. - -To start a working group: - -1. Decide what the working group is going to focus on. This should be tightly focused and achievable! -2. Gather at least 3 people including yourself who are willing to be in the working group. -3. Ping the `@Maintainer` role on Discord in [#engine-dev](https://discord.com/channels/691052431525675048/692572690833473578) announcing your mutual intent and a one or two sentence description of your plans. - -The maintainers will briefly evaluate the proposal in consultation with the relevant SMEs and give you a thumbs up or down on whether this is something Bevy can and wants to explore right now. -You don't need a concrete plan at this stage, just a sensible argument for both "why is this something that could be useful to Bevy" and "why there aren't any serious barriers in implementing this in the near future". -If they're in favor, a maintainer will create a forum channel for you and you're off to the races. - -Your initial task is writing up a design doc: laying out the scope of work and general implementation strategy. -Here's a [solid example of a design doc](https://github.com/bevyengine/bevy/issues/12365), although feel free to use whatever format works best for your team. - -Once that's ready, get a sign-off on the broad vision and goals from the appropriate SMEs and maintainers. -This is the primary review step: maintainers and SMEs should be broadly patient and supportive even if they're skeptical until a proper design doc is in hand to evaluate. - -With a sign-off in hand, post the design doc to [Github Discussions](https://github.com/bevyengine/bevy/discussions) with the [`C-Design-Doc` label](https://github.com/bevyengine/bevy/discussions?discussions_q=is%3Aopen+label%3A%22C-Design+Doc%22) for archival purposes and begin work on implementation. -Post PRs that you need reviews on in your group's forum thread, ask for advice, and share the load. -Controversial PRs are still `S-Controversial`, but with a sign-off-in-principle, things should go more smoothly. - -If work peters out and the initiative dies, maintainers can wind down working groups (in consultation with SMEs and the working group itself). -This is normal and expected: projects fail for all sorts of reasons! -However, it's important to both keep the number of working groups relatively small and ensure they're active: -they serve a vital role in onboarding new contributors. - -Once your implementation work laid out in your initial design doc is complete, it's time to wind down the working group. -Feel free to make another one though to tackle the next step in your grand vision! - -### Battle-testing Bevy - -Ultimately, Bevy is a tool that's designed to help people make cool games. -By using Bevy, you can help us catch bugs, prioritize new features, polish off the rough edges, and promote the project. - -If you need help, don't hesitate to ask for help on [GitHub Discussions], [Discord], or [reddit](https://www.reddit.com/r/bevy). Generally you should prefer asking questions as [GitHub Discussions] as they are more searchable. - -When you think you've found a bug, missing documentation, or a feature that would help you make better games, please [file an issue](https://github.com/bevyengine/bevy/issues/new/choose) on the main `bevy` repo. - -Do your best to search for duplicate issues, but if you're unsure, open a new issue and link to other related issues on the thread you make. - -Once you've made something that you're proud of, feel free to drop a link, video, or screenshot in `#showcase` on [Discord]! -If you release a game on [itch.io](https://itch.io/games/tag-bevy) we'd be thrilled if you tagged it with `bevy`. - -### Teaching others - -Bevy is still very young, and light on documentation, tutorials, and accumulated expertise. -By helping others with their issues, and teaching them about Bevy, you will naturally learn the engine and codebase in greater depth while also making our community better! - -Some of the best ways to do this are: - -* Answering questions on [GitHub Discussions], [Discord], and [reddit](https://www.reddit.com/r/bevy). -* Writing tutorials, guides, and other informal documentation and sharing them on [Bevy Assets](https://github.com/bevyengine/bevy-assets). -* Streaming, writing blog posts about creating your game, and creating videos. Share these in the `#devlogs` channel on [Discord]! - -### Writing plugins - -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://bevyengine.org/learn/book/plugin-development/) for helpful tips and patterns. - -### Fixing bugs - -Bugs in Bevy (or the associated website / book) are filed on the issue tracker using the [`C-Bug`](https://github.com/bevyengine/bevy/issues?q=is%3Aissue+is%3Aopen+label%3AC-Bug) label. - -If you're looking for an easy place to start, take a look at the [`D-Good-First-Issue`](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AD-Good-First-Issue) label, and feel free to ask questions on that issue's thread in question or on [Discord]. -You don't need anyone's permission to try fixing a bug or adding a simple feature, but stating that you'd like to tackle an issue can be helpful to avoid duplicated work. - -When you make a pull request that fixes an issue, include a line that says `Fixes #X` (or "Closes"), where `X` is the issue number. -This will cause the issue in question to be closed when your PR is merged. - -General improvements to code quality are also welcome! -Bevy can always be safer, better tested, and more idiomatic. - -### Writing docs - -Like every other large, rapidly developing open source library you've ever used, Bevy's documentation can always use improvement. -This is incredibly valuable, easily distributed work, but requires a bit of guidance: - -* Inaccurate documentation is worse than no documentation: prioritize fixing broken docs. -* Bevy is remarkably unstable: before tackling a new major documentation project, check in with the community on Discord or GitHub (making an issue about specific missing docs is a great way to plan) about the stability of that feature and upcoming plans to save yourself heartache. -* Code documentation (doc examples and in the examples folder) is easier to maintain because the compiler will tell us when it breaks. -* Inline documentation should be technical and to the point. Link relevant examples or other explanations if broader context is useful. -* The Bevy book is hosted on the `bevy-website` repo and targeted towards beginners who are just getting to know Bevy (and perhaps Rust!). -* Accepted RFCs are not documentation: they serve only as a record of accepted decisions. - -[docs.rs](https://docs.rs/bevy) is built from out of the last release's documentation, which is written right in-line directly above the code it documents. -To view the current docs on `main` before you contribute, clone the `bevy` repo, and run `cargo doc --open` or go to [dev-docs.bevyengine.org](https://dev-docs.bevyengine.org/), -which has the latest API reference built from the repo on every commit made to the `main` branch. - -### 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. - -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. - -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). - -### Reviewing others' work - -With the sheer volume of activity in Bevy's community, reviewing others work with the aim of improving it is one of the most valuable things you can do. -You don't need to be an Elder Rustacean to be useful here: anyone can catch missing tests, unclear docs, logic errors, and so on. -If you have specific skills (e.g. advanced familiarity with `unsafe` code, rendering knowledge or web development experience) or personal experience with a problem, try to prioritize those areas to ensure we can get appropriate expertise where we need it. - -When you find (or make) a PR that you don't feel comfortable reviewing, but you *can* think of someone who does, consider using Github's "Request review" functionality (in the top-right of the PR screen) to bring the work to their attention. -If they're not a Bevy Org member, you'll need to ping them in the thread directly: that's fine too! -Almost everyone working on Bevy is a volunteer: this should be treated as a gentle nudge, rather than an assignment of work. -Consider checking the Git history for appropriate reviewers, or ask on Discord for suggestions. - -Focus on giving constructive, actionable feedback that results in real improvements to code quality or end-user experience. -If you don't understand why an approach was taken, please ask! - -Provide actual code suggestions when that is helpful. Small changes work well as comments or in-line suggestions on specific lines of codes. -Larger changes deserve a comment in the main thread, or a pull request to the original author's branch (but please mention that you've made one). -When in doubt about a matter of architectural philosophy, refer back to [*What we're trying to build*](#what-were-trying-to-build) for guidance. - -Once you're happy with the work and feel you're reasonably qualified to assess quality in this particular area, leave your `Approved` review on the PR. -If you're new to GitHub, check out the [Pull Request Review documentation](https://docs.github.com/en/github/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/about-pull-request-reviews). -**Anyone** can and should leave reviews ... no special permissions are required! - -It's okay to leave an approval even if you aren't 100% confident on all areas of the PR: just be sure to note your limitations. -When maintainers are evaluating the PR to be merged, they'll make sure that there's good coverage on all of the critical areas. -If you can only check that the math is correct, and another reviewer can check everything *but* the math, we're in good shape! - -Similarly, if there are areas that would be *good* to fix but aren't severe, please consider leaving an approval. -The author can address them immediately, or spin it out into follow-up issues or PRs. -Large PRs are much more draining for both reviewers and authors, so try to push for a smaller scope with clearly tracked follow-ups. - -There are three main places you can check for things to review: - -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. - -Not even our Project Leads and Maintainers are exempt from reviews and RFCs! -By giving feedback on this work (and related supporting work), you can help us make sure our releases are both high-quality and timely. - -Finally, if nothing brings you more satisfaction than seeing every last issue labeled and all resolved issues closed, feel free to message the Project Lead (currently @cart) for a Bevy org role to help us keep things tidy. -As discussed in our [*Bevy Organization doc*](/docs/the_bevy_organization.md), this role only requires good faith and a basic understanding of our development process. - -### How to adopt pull requests - -Occasionally authors of pull requests get busy or become unresponsive, or project members fail to reply in a timely manner. -This is a natural part of any open source project. -To avoid blocking these efforts, these pull requests may be *adopted*, where another contributor creates a new pull request with the same content. -If there is an old pull request that is without updates, comment to the organization whether it is appropriate to add the -*[S-Adopt-Me](https://github.com/bevyengine/bevy/labels/S-Adopt-Me)* label, to indicate that it can be *adopted*. -If you plan on adopting a PR yourself, you can also leave a comment on the PR asking the author if they plan on returning. -If the author gives permission or simply doesn't respond after a few days, then it can be adopted. -This may sometimes even skip the labeling process since at that point the PR has been adopted by you. - -With this label added, it's best practice to fork the original author's branch. -This ensures that they still get credit for working on it and that the commit history is retained. -When the new pull request is ready, it should reference the original PR in the description. -Then notify org members to close the original. - -* For example, you can reference the original PR by adding the following to your PR description: - -`Adopted #number-original-pull-request` - -### Contributing code - -Bevy is actively open to code contributions from community members. -If you're new to Bevy, here's the workflow we use: - -1. Fork the `bevyengine/bevy` repository on GitHub. You'll need to create a GitHub account if you don't have one already. -2. Make your changes in a local clone of your fork, typically in its own new branch. - 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). -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: - 1. Install [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli). - 2. Run `markdownlint -f -c .github/linters/.markdown-lint.yml .` in the root directory of the Bevy project. -5. When working with Toml (`.toml`) files, Bevy's CI will check toml files using [taplo](https://taplo.tamasfe.dev/): `taplo fmt --check --diff` - 1. If you use VSCode, install [Even better toml](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) and format your files. - 2. If you want to use the cli tool, install [taplo-cli](https://taplo.tamasfe.dev/cli/installation/cargo.html) and run `taplo fmt --check --diff` to check for the formatting. Fix any issues by running `taplo fmt` in the root directory of the Bevy project. -6. Check for typos. Bevy's CI will check for them using [typos](https://github.com/crate-ci/typos). - 1. If you use VSCode, install [Typos Spell Checker](https://marketplace.visualstudio.com/items?itemName=tekumara.typos-vscode). - 2. You can also use the cli tool. Install [typos-cli](https://github.com/crate-ci/typos?tab=readme-ov-file#install) and run `typos` to check for typos, and fix them by running `typos -w`. -7. Push your changes to your fork on Github and open a Pull Request. -8. Respond to any CI failures or review feedback. While CI failures must be fixed before we can merge your PR, you do not need to *agree* with all feedback from your reviews, merely acknowledge that it was given. If you cannot come to an agreement, leave the thread open and defer to a Maintainer or Project Lead's final judgement. -9. When your PR is ready to merge, a Maintainer or Project Lead will review it and suggest final changes. If those changes are minimal they may even apply them directly to speed up merging. - -If you end up adding a new official Bevy crate to the `bevy` repo: - -1. Add the new crate to the [./tools/publish.sh](./tools/publish.sh) file. -2. Check if a new cargo feature was added, update [cargo_features.md](https://github.com/bevyengine/bevy/blob/main/docs/cargo_features.md) as needed. - -When contributing, please: - -* Try to loosely follow the workflow in [*Making changes to Bevy*](#making-changes-to-bevy). -* Consult the [style guide](.github/contributing/engine_style_guide.md) to help keep our code base tidy. -* Explain what you're doing and why. -* Document new code with doc comments. -* Include clear, simple tests. -* Add or improve the examples when adding new user-facing functionality. -* Break work into digestible chunks. -* Ask for any help that you need! - -Your first PR will be merged in no time! - -No matter how you're helping: thanks for contributing to Bevy! - -[GitHub Discussions]: https://github.com/bevyengine/bevy/discussions "GitHub Discussions" -[Discord]: https://discord.gg/bevy "Discord" +Hey, we've moved our information on contributing to Bevy's website [here](https://bevyengine.org/learn/contribute/introduction). Go give it a read, and thanks for contributing! diff --git a/Cargo.toml b/Cargo.toml index ad0ca5a2937956..474058b45f4476 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,14 +31,14 @@ members = [ ] [workspace.lints.clippy] -type_complexity = "allow" doc_markdown = "warn" manual_let_else = "warn" -undocumented_unsafe_blocks = "warn" -redundant_else = "warn" match_same_arms = "warn" -semicolon_if_nothing_returned = "warn" redundant_closure_for_method_calls = "warn" +redundant_else = "warn" +semicolon_if_nothing_returned = "warn" +type_complexity = "allow" +undocumented_unsafe_blocks = "warn" unwrap_or_default = "warn" ptr_as_ptr = "warn" @@ -46,9 +46,11 @@ ptr_cast_constness = "warn" ref_as_ptr = "warn" [workspace.lints.rust] -unsafe_op_in_unsafe_fn = "warn" missing_docs = "warn" +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" +unsafe_op_in_unsafe_fn = "warn" +unused_qualifications = "warn" [lints] workspace = true @@ -110,9 +112,6 @@ bevy_core_pipeline = [ "bevy_render", ] -# Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading)) -bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"] - # Adds gamepad support bevy_gilrs = ["bevy_internal/bevy_gilrs"] @@ -348,10 +347,16 @@ ios_simulator = ["bevy_internal/ios_simulator"] # Enable built in global state machines bevy_state = ["bevy_internal/bevy_state"] +# Enables source location tracking for change detection, which can assist with debugging +track_change_detection = ["bevy_internal/track_change_detection"] + +# Enable function reflection +reflect_functions = ["bevy_internal/reflect_functions"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.15.0-dev", default-features = false } -# WASM does not support dynamic linking. +# Wasm does not support dynamic linking. [target.'cfg(not(target_family = "wasm"))'.dependencies] bevy_dylib = { path = "crates/bevy_dylib", version = "0.15.0-dev", default-features = false, optional = true } @@ -362,6 +367,7 @@ ron = "0.8.0" flate2 = "1.0" serde = { version = "1", features = ["derive"] } bytemuck = "1.7" +bevy_render = { path = "crates/bevy_render", version = "0.15.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" async-std = "1.12" @@ -592,6 +598,17 @@ description = "Demonstrates transparency in 2d" category = "2D Rendering" wasm = true +[[example]] +name = "mesh2d_alpha_mode" +path = "examples/2d/mesh2d_alpha_mode.rs" +doc-scrape-examples = true + +[package.metadata.example.mesh2d_alpha_mode] +name = "Mesh2d Alpha Mode" +description = "Used to test alpha modes with mesh2d" +category = "2D Rendering" +wasm = true + [[example]] name = "pixel_grid_snap" path = "examples/2d/pixel_grid_snap.rs" @@ -756,7 +773,8 @@ doc-scrape-examples = true name = "Lines" description = "Create a custom material to draw 3d lines" category = "3D Rendering" -wasm = true +# Wasm does not support the `POLYGON_MODE_LINE` feature. +wasm = false [[example]] name = "ssao" @@ -1060,7 +1078,7 @@ setup = [ "curl", "-o", "assets/models/bunny.meshlet_mesh", - "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/bd869887bc5c9c6e74e353f657d342bef84bacd8/bunny.meshlet_mesh", + "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh", ], ] @@ -1587,6 +1605,17 @@ description = "Shows how to create a custom diagnostic" category = "Diagnostics" wasm = true +[[example]] +name = "enabling_disabling_diagnostic" +path = "examples/diagnostics/enabling_disabling_diagnostic.rs" +doc-scrape-examples = true + +[package.metadata.example.enabling_disabling_diagnostic] +name = "Enabling/disabling diagnostic" +description = "Shows how to disable/re-enable a Diagnostic during runtime" +category = "Diagnostics" +wasm = true + # ECS (Entity Component System) [[example]] name = "ecs_guide" @@ -1606,13 +1635,14 @@ category = "ECS (Entity Component System)" wasm = false [[example]] -name = "component_change_detection" -path = "examples/ecs/component_change_detection.rs" +name = "change_detection" +path = "examples/ecs/change_detection.rs" doc-scrape-examples = true +required-features = ["track_change_detection"] -[package.metadata.example.component_change_detection] -name = "Component Change Detection" -description = "Change detection on components" +[package.metadata.example.change_detection] +name = "Change Detection" +description = "Change detection on components and resources" category = "ECS (Entity Component System)" wasm = false @@ -2158,6 +2188,7 @@ wasm = false name = "function_reflection" path = "examples/reflection/function_reflection.rs" doc-scrape-examples = true +required-features = ["reflect_functions"] [package.metadata.example.function_reflection] name = "Function Reflection" @@ -2188,13 +2219,13 @@ category = "Reflection" wasm = false [[example]] -name = "trait_reflection" -path = "examples/reflection/trait_reflection.rs" +name = "type_data" +path = "examples/reflection/type_data.rs" doc-scrape-examples = true -[package.metadata.example.trait_reflection] -name = "Trait Reflection" -description = "Allows reflection with trait objects" +[package.metadata.example.type_data] +name = "Type Data" +description = "Demonstrates how to create and use type data" category = "Reflection" wasm = false @@ -2233,11 +2264,11 @@ category = "Shaders" wasm = true [[example]] -name = "post_processing" -path = "examples/shader/post_processing.rs" +name = "custom_post_processing" +path = "examples/shader/custom_post_processing.rs" doc-scrape-examples = true -[package.metadata.example.post_processing] +[package.metadata.example.custom_post_processing] name = "Post Processing - Custom Render Pass" description = "A custom post processing effect, using a custom render pass that runs after the main pass" category = "Shaders" @@ -2387,6 +2418,17 @@ description = "A shader that shows how to bind and sample multiple textures as a category = "Shaders" wasm = false +[[example]] +name = "specialized_mesh_pipeline" +path = "examples/shader/specialized_mesh_pipeline.rs" +doc-scrape-examples = true + +[package.metadata.example.specialized_mesh_pipeline] +name = "Specialized Mesh Pipeline" +description = "Demonstrates how to write a specialized mesh pipeline" +category = "Shaders" +wasm = true + # Stress tests [[package.metadata.example_category]] name = "Stress Tests" @@ -2566,6 +2608,17 @@ description = "Demonstrates observers that react to events (both built-in life-c category = "ECS (Entity Component System)" wasm = true +[[example]] +name = "observer_propagation" +path = "examples/ecs/observer_propagation.rs" +doc-scrape-examples = true + +[package.metadata.example.observer_propagation] +name = "Observer Propagation" +description = "Demonstrates event propagation with observers" +category = "ECS (Entity Component System)" +wasm = true + [[example]] name = "3d_rotation" path = "examples/transforms/3d_rotation.rs" @@ -2633,17 +2686,6 @@ description = "Demonstrates how to create a node with a border" category = "UI (User Interface)" wasm = true -[[example]] -name = "rounded_borders" -path = "examples/ui/rounded_borders.rs" -doc-scrape-examples = true - -[package.metadata.example.rounded_borders] -name = "Rounded Borders" -description = "Demonstrates how to create a node with a rounded border" -category = "UI (User Interface)" -wasm = true - [[example]] name = "button" path = "examples/ui/button.rs" @@ -2976,6 +3018,14 @@ description = "Demonstrates customizing default window settings" category = "Window" wasm = true +[[example]] +name = "ambiguity_detection" +path = "tests/ecs/ambiguity_detection.rs" +doc-scrape-examples = true + +[package.metadata.example.ambiguity_detection] +hidden = true + [[example]] name = "resizing" path = "tests/window/resizing.rs" @@ -3033,6 +3083,17 @@ description = "Demonstrates creating and using custom Ui materials" category = "UI (User Interface)" wasm = true +[[example]] +name = "cubic_splines" +path = "examples/math/cubic_splines.rs" +doc-scrape-examples = true + +[package.metadata.example.cubic_splines] +name = "Cubic Splines" +description = "Exhibits different modes of constructing cubic curves using splines" +category = "Math" +wasm = true + [[example]] name = "render_primitives" path = "examples/math/render_primitives.rs" @@ -3258,6 +3319,28 @@ description = "Demonstrates how to enqueue custom draw commands in a render phas category = "Shaders" wasm = true +[[example]] +name = "fog_volumes" +path = "examples/3d/fog_volumes.rs" +doc-scrape-examples = true + +[package.metadata.example.fog_volumes] +name = "Fog volumes" +description = "Demonstrates fog volumes" +category = "3D Rendering" +wasm = false + +[[example]] +name = "scrolling_fog" +path = "examples/3d/scrolling_fog.rs" +doc-scrape-examples = true + +[package.metadata.example.scrolling_fog] +name = "Scrolling fog" +description = "Demonstrates how to create the effect of fog moving in the wind" +category = "3D Rendering" +wasm = false + [[example]] name = "physics_in_fixed_timestep" path = "examples/movement/physics_in_fixed_timestep.rs" @@ -3269,6 +3352,41 @@ description = "Handles input, physics, and rendering in an industry-standard way category = "Movement" wasm = true +[[example]] +name = "post_processing" +path = "examples/3d/post_processing.rs" +doc-scrape-examples = true + +[package.metadata.example.post_processing] +name = "Built-in postprocessing" +description = "Demonstrates the built-in postprocessing features" +category = "3D Rendering" +wasm = true + +[[example]] +name = "rotate_environment_map" +path = "examples/3d/rotate_environment_map.rs" +doc-scrape-examples = true +required-features = ["pbr_multi_layer_material_textures"] + +[package.metadata.example.rotate_environment_map] +name = "Rotate Environment Map" +description = "Demonstrates how to rotate the skybox and the environment map simultaneously" +category = "3D Rendering" +wasm = false + +[[example]] +name = "simple_picking" +path = "examples/picking/simple_picking.rs" +doc-scrape-examples = true +required-features = ["bevy_picking"] + +[package.metadata.example.simple_picking] +name = "Showcases simple picking events and usage" +description = "Demonstrates how to use picking events to spawn simple objects" +category = "Picking" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" @@ -3281,6 +3399,22 @@ lto = "fat" panic = "abort" [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +# This cfg is needed so that #[doc(fake_variadic)] is correctly propagated for +# impls for re-exported traits. See https://github.com/rust-lang/cargo/issues/8811 +# for details on why this is needed. Since dependencies don't expect to be built +# with `--cfg docsrs` (and thus fail to compile) we use a different cfg. +rustc-args = ["--cfg", "docsrs_dep"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] + +[[example]] +name = "monitor_info" +path = "examples/window/monitor_info.rs" +doc-scrape-examples = true + +[package.metadata.example.monitor_info] +name = "Monitor info" +description = "Displays information about available monitors (displays)." +category = "Window" +wasm = false diff --git a/assets/shaders/specialized_mesh_pipeline.wgsl b/assets/shaders/specialized_mesh_pipeline.wgsl new file mode 100644 index 00000000000000..82b5cea9116588 --- /dev/null +++ b/assets/shaders/specialized_mesh_pipeline.wgsl @@ -0,0 +1,48 @@ +//! Very simple shader used to demonstrate how to get the world position and pass data +//! between the vertex and fragment shader. Also shows the custom vertex layout. + +// First we import everything we need from bevy_pbr +// A 2d shader would be vevry similar but import from bevy_sprite instead +#import bevy_pbr::{ + mesh_functions, + view_transformations::position_world_to_clip +} + +struct Vertex { + // This is needed if you are using batching and/or gpu preprocessing + // It's a built in so you don't need to define it in the vertex layout + @builtin(instance_index) instance_index: u32, + // Like we defined for the vertex layout + // position is at location 0 + @location(0) position: vec3, + // and color at location 1 + @location(1) color: vec4, +}; + +// This is the output of the vertex shader and we also use it as the input for the fragment shader +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_position: vec4, + @location(1) color: vec3, +}; + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + // This is how bevy computes the world position + // The vertex.instance_index is very important. Esepecially if you are using batching and gpu preprocessing + var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.clip_position = position_world_to_clip(out.world_position.xyz); + + // We just use the raw vertex color + out.color = vertex.color.rgb; + + return out; +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + // output the color directly + return vec4(in.color, 1.0); +} \ No newline at end of file diff --git a/assets/volumes/bunny.ktx2 b/assets/volumes/bunny.ktx2 new file mode 100644 index 00000000000000..421b20791565a0 Binary files /dev/null and b/assets/volumes/bunny.ktx2 differ diff --git a/assets/volumes/fog_noise.ktx2 b/assets/volumes/fog_noise.ktx2 new file mode 100644 index 00000000000000..0f17706c00581d Binary files /dev/null and b/assets/volumes/fog_noise.ktx2 differ diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 3df074a75c0452..5e28ffb444612b 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -6,17 +6,22 @@ publish = false license = "MIT OR Apache-2.0" [dev-dependencies] -glam = "0.27" +glam = "0.28" rand = "0.8" rand_chacha = "0.3" criterion = { version = "0.3", features = ["html_reports"] } bevy_app = { path = "../crates/bevy_app" } bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] } -bevy_reflect = { path = "../crates/bevy_reflect" } -bevy_tasks = { path = "../crates/bevy_tasks" } -bevy_utils = { path = "../crates/bevy_utils" } +bevy_hierarchy = { path = "../crates/bevy_hierarchy" } bevy_math = { path = "../crates/bevy_math" } +bevy_reflect = { path = "../crates/bevy_reflect", features = ["functions"] } bevy_render = { path = "../crates/bevy_render" } +bevy_tasks = { path = "../crates/bevy_tasks" } +bevy_utils = { path = "../crates/bevy_utils" } + +# make bevy_render compile on linux. x11 vs wayland does not matter here as the benches do not actually use a window +[target.'cfg(target_os = "linux")'.dev-dependencies] +bevy_winit = { path = "../crates/bevy_winit", features = ["x11"] } [profile.release] opt-level = 3 @@ -32,6 +37,11 @@ name = "ecs" path = "benches/bevy_ecs/benches.rs" harness = false +[[bench]] +name = "reflect_function" +path = "benches/bevy_reflect/function.rs" +harness = false + [[bench]] name = "reflect_list" path = "benches/bevy_reflect/list.rs" diff --git a/benches/benches/bevy_ecs/benches.rs b/benches/benches/bevy_ecs/benches.rs index 6f1e89fb6d316c..1392536a7da0bf 100644 --- a/benches/benches/bevy_ecs/benches.rs +++ b/benches/benches/bevy_ecs/benches.rs @@ -2,7 +2,9 @@ use criterion::criterion_main; mod components; mod events; +mod fragmentation; mod iteration; +mod observers; mod scheduling; mod world; @@ -10,6 +12,8 @@ criterion_main!( components::components_benches, events::event_benches, iteration::iterations_benches, + fragmentation::fragmentation_benches, + observers::observer_benches, scheduling::scheduling_benches, world::world_benches, ); diff --git a/benches/benches/bevy_ecs/fragmentation/mod.rs b/benches/benches/bevy_ecs/fragmentation/mod.rs new file mode 100644 index 00000000000000..c5b364622139cd --- /dev/null +++ b/benches/benches/bevy_ecs/fragmentation/mod.rs @@ -0,0 +1,99 @@ +use bevy_ecs::prelude::*; +use bevy_ecs::system::SystemState; +use criterion::*; +use glam::*; +use std::hint::black_box; + +criterion_group!(fragmentation_benches, iter_frag_empty); + +#[derive(Component, Default)] +struct Table(usize); +#[derive(Component, Default)] +#[component(storage = "SparseSet")] +struct Sparse(usize); + +fn flip_coin() -> bool { + rand::random::() +} +fn iter_frag_empty(c: &mut Criterion) { + let mut group = c.benchmark_group("iter_fragmented(4096)_empty"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + group.bench_function("foreach_table", |b| { + let mut world = World::new(); + spawn_empty_frag_archetype::(&mut world); + let mut q: SystemState> = + SystemState::)>>::new(&mut world); + let query = q.get(&world); + b.iter(move || { + let mut res = 0; + query.iter().for_each(|(e, t)| { + res += e.to_bits(); + black_box(t); + }); + }); + }); + group.bench_function("foreach_sparse", |b| { + let mut world = World::new(); + spawn_empty_frag_archetype::(&mut world); + let mut q: SystemState> = + SystemState::)>>::new(&mut world); + let query = q.get(&world); + b.iter(move || { + let mut res = 0; + query.iter().for_each(|(e, t)| { + res += e.to_bits(); + black_box(t); + }); + }); + }); + group.finish(); + + fn spawn_empty_frag_archetype(world: &mut World) { + for i in 0..65536 { + let mut e = world.spawn_empty(); + if flip_coin() { + e.insert(Table::<1>(0)); + } + if flip_coin() { + e.insert(Table::<2>(0)); + } + if flip_coin() { + e.insert(Table::<3>(0)); + } + if flip_coin() { + e.insert(Table::<4>(0)); + } + if flip_coin() { + e.insert(Table::<5>(0)); + } + if flip_coin() { + e.insert(Table::<6>(0)); + } + if flip_coin() { + e.insert(Table::<7>(0)); + } + if flip_coin() { + e.insert(Table::<8>(0)); + } + if flip_coin() { + e.insert(Table::<9>(0)); + } + if flip_coin() { + e.insert(Table::<10>(0)); + } + if flip_coin() { + e.insert(Table::<11>(0)); + } + if flip_coin() { + e.insert(Table::<12>(0)); + } + e.insert(T::default()); + + if i != 0 { + e.despawn(); + } + } + } +} diff --git a/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs b/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs new file mode 100644 index 00000000000000..73eb55cfdbf8ee --- /dev/null +++ b/benches/benches/bevy_ecs/iteration/iter_simple_foreach_hybrid.rs @@ -0,0 +1,43 @@ +use bevy_ecs::prelude::*; +use rand::{prelude::SliceRandom, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +#[derive(Component, Copy, Clone)] +struct TableData(f32); + +#[derive(Component, Copy, Clone)] +#[component(storage = "SparseSet")] +struct SparseData(f32); + +fn deterministic_rand() -> ChaCha8Rng { + ChaCha8Rng::seed_from_u64(42) +} +pub struct Benchmark<'w>(World, QueryState<(&'w mut TableData, &'w SparseData)>); + +impl<'w> Benchmark<'w> { + pub fn new() -> Self { + let mut world = World::new(); + + let mut v = vec![]; + for _ in 0..10000 { + world.spawn((TableData(0.0), SparseData(0.0))).id(); + v.push(world.spawn(TableData(0.)).id()); + } + + // by shuffling ,randomize the archetype iteration order to significantly deviate from the table order. This maximizes the loss of cache locality during archetype-based iteration. + v.shuffle(&mut deterministic_rand()); + for e in v.into_iter() { + world.entity_mut(e).despawn(); + } + + let query = world.query::<(&mut TableData, &SparseData)>(); + Self(world, query) + } + + #[inline(never)] + pub fn run(&mut self) { + self.1 + .iter_mut(&mut self.0) + .for_each(|(mut v1, v2)| v1.0 += v2.0) + } +} diff --git a/benches/benches/bevy_ecs/iteration/mod.rs b/benches/benches/bevy_ecs/iteration/mod.rs index 790884335021e5..baa1bb385bb87d 100644 --- a/benches/benches/bevy_ecs/iteration/mod.rs +++ b/benches/benches/bevy_ecs/iteration/mod.rs @@ -11,6 +11,7 @@ mod iter_frag_wide; mod iter_frag_wide_sparse; mod iter_simple; mod iter_simple_foreach; +mod iter_simple_foreach_hybrid; mod iter_simple_foreach_sparse_set; mod iter_simple_foreach_wide; mod iter_simple_foreach_wide_sparse_set; @@ -71,6 +72,10 @@ fn iter_simple(c: &mut Criterion) { let mut bench = iter_simple_foreach_wide_sparse_set::Benchmark::new(); b.iter(move || bench.run()); }); + group.bench_function("foreach_hybrid", |b| { + let mut bench = iter_simple_foreach_hybrid::Benchmark::new(); + b.iter(move || bench.run()); + }); group.finish(); } diff --git a/benches/benches/bevy_ecs/observers/mod.rs b/benches/benches/bevy_ecs/observers/mod.rs new file mode 100644 index 00000000000000..0b8c3f24869ce0 --- /dev/null +++ b/benches/benches/bevy_ecs/observers/mod.rs @@ -0,0 +1,8 @@ +use criterion::criterion_group; + +mod propagation; +mod simple; +use propagation::*; +use simple::*; + +criterion_group!(observer_benches, event_propagation, observe_simple); diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs new file mode 100644 index 00000000000000..06d2d45d2161d0 --- /dev/null +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -0,0 +1,128 @@ +use bevy_ecs::{ + component::Component, + entity::Entity, + event::{Event, EventWriter}, + observer::Trigger, + world::World, +}; +use bevy_hierarchy::{BuildChildren, Children, Parent}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::{prelude::SliceRandom, SeedableRng}; +use rand::{seq::IteratorRandom, Rng}; +use rand_chacha::ChaCha8Rng; + +const DENSITY: usize = 20; // percent of nodes with listeners +const ENTITY_DEPTH: usize = 64; +const ENTITY_WIDTH: usize = 200; +const N_EVENTS: usize = 500; +fn deterministic_rand() -> ChaCha8Rng { + ChaCha8Rng::seed_from_u64(42) +} + +pub fn event_propagation(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("event_propagation"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + group.bench_function("single_event_type", |bencher| { + let mut world = World::new(); + let (roots, leaves, nodes) = spawn_listener_hierarchy(&mut world); + add_listeners_to_hierarchy::(&roots, &leaves, &nodes, &mut world); + + bencher.iter(|| { + send_events::<1, N_EVENTS>(&mut world, &leaves); + }); + }); + + group.bench_function("single_event_type_no_listeners", |bencher| { + let mut world = World::new(); + let (roots, leaves, nodes) = spawn_listener_hierarchy(&mut world); + add_listeners_to_hierarchy::(&roots, &leaves, &nodes, &mut world); + + bencher.iter(|| { + // no listeners to observe TestEvent<9> + send_events::<9, N_EVENTS>(&mut world, &leaves); + }); + }); + + group.bench_function("four_event_types", |bencher| { + let mut world = World::new(); + let (roots, leaves, nodes) = spawn_listener_hierarchy(&mut world); + const FRAC_N_EVENTS_4: usize = N_EVENTS / 4; + const FRAC_DENSITY_4: usize = DENSITY / 4; + add_listeners_to_hierarchy::(&roots, &leaves, &nodes, &mut world); + add_listeners_to_hierarchy::(&roots, &leaves, &nodes, &mut world); + add_listeners_to_hierarchy::(&roots, &leaves, &nodes, &mut world); + add_listeners_to_hierarchy::(&roots, &leaves, &nodes, &mut world); + + bencher.iter(|| { + send_events::<1, FRAC_N_EVENTS_4>(&mut world, &leaves); + send_events::<2, FRAC_N_EVENTS_4>(&mut world, &leaves); + send_events::<3, FRAC_N_EVENTS_4>(&mut world, &leaves); + send_events::<4, FRAC_N_EVENTS_4>(&mut world, &leaves); + }); + }); + + group.finish(); +} + +#[derive(Clone, Component)] +struct TestEvent {} + +impl Event for TestEvent { + type Traversal = Parent; + const AUTO_PROPAGATE: bool = true; +} + +fn send_events(world: &mut World, leaves: &Vec) { + let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap(); + + (0..N_EVENTS).for_each(|_| { + world.trigger_targets(TestEvent:: {}, *target); + }); +} + +fn spawn_listener_hierarchy(world: &mut World) -> (Vec, Vec, Vec) { + let mut roots = vec![]; + let mut leaves = vec![]; + let mut nodes = vec![]; + for _ in 0..ENTITY_WIDTH { + let mut parent = world.spawn_empty().id(); + roots.push(parent); + for _ in 0..ENTITY_DEPTH { + let child = world.spawn_empty().id(); + nodes.push(child); + + world.entity_mut(parent).add_child(child); + parent = child; + } + nodes.pop(); + leaves.push(parent); + } + (roots, leaves, nodes) +} + +fn add_listeners_to_hierarchy( + roots: &Vec, + leaves: &Vec, + nodes: &Vec, + world: &mut World, +) { + for e in roots.iter() { + world.entity_mut(*e).observe(empty_listener::); + } + for e in leaves.iter() { + world.entity_mut(*e).observe(empty_listener::); + } + let mut rng = deterministic_rand(); + for e in nodes.iter() { + if rng.gen_bool(DENSITY as f64 / 100.0) { + world.entity_mut(*e).observe(empty_listener::); + } + } +} + +fn empty_listener(trigger: Trigger>) { + black_box(trigger); +} diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs new file mode 100644 index 00000000000000..6da8669456407e --- /dev/null +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -0,0 +1,49 @@ +use bevy_ecs::{entity::Entity, event::Event, observer::Trigger, world::World}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::{prelude::SliceRandom, SeedableRng}; +use rand_chacha::ChaCha8Rng; +fn deterministic_rand() -> ChaCha8Rng { + ChaCha8Rng::seed_from_u64(42) +} + +#[derive(Clone, Event)] +struct EventBase; + +pub fn observe_simple(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("observe"); + group.warm_up_time(std::time::Duration::from_millis(500)); + group.measurement_time(std::time::Duration::from_secs(4)); + + group.bench_function("trigger_simple", |bencher| { + let mut world = World::new(); + world.observe(empty_listener_base); + bencher.iter(|| { + for _ in 0..10000 { + world.trigger(EventBase) + } + }); + }); + + group.bench_function("trigger_targets_simple/10000_entity", |bencher| { + let mut world = World::new(); + let mut entities = vec![]; + for _ in 0..10000 { + entities.push(world.spawn_empty().observe(empty_listener_base).id()); + } + entities.shuffle(&mut deterministic_rand()); + bencher.iter(|| { + send_base_event(&mut world, &entities); + }); + }); + + group.finish(); +} + +fn empty_listener_base(trigger: Trigger) { + black_box(trigger); +} + +fn send_base_event(world: &mut World, entities: &Vec) { + world.trigger_targets(EventBase, entities); +} diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 2b3d84195aff8c..75b4b25a2a2aee 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -1,3 +1,4 @@ +use std::mem::size_of; use bevy_ecs::{ component::Component, entity::Entity, @@ -184,8 +185,7 @@ impl Default for LargeStruct { } pub fn sized_commands_impl(criterion: &mut Criterion) { - let mut group = - criterion.benchmark_group(format!("sized_commands_{}_bytes", std::mem::size_of::())); + let mut group = criterion.benchmark_group(format!("sized_commands_{}_bytes", size_of::())); group.warm_up_time(std::time::Duration::from_millis(500)); group.measurement_time(std::time::Duration::from_secs(4)); diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index 10645a59977a76..69590aa80412d8 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -20,7 +20,8 @@ fn cubic_2d(c: &mut Criterion) { vec2(1.0, 0.0), vec2(1.0, 1.0), ]]) - .to_curve(); + .to_curve() + .expect("Unable to build a curve from this data"); c.bench_function("cubic_position_Vec2", |b| { b.iter(|| black_box(bezier.position(black_box(0.5)))); }); @@ -33,7 +34,8 @@ fn cubic(c: &mut Criterion) { vec3a(1.0, 0.0, 0.0), vec3a(1.0, 1.0, 1.0), ]]) - .to_curve(); + .to_curve() + .expect("Unable to build a curve from this data"); c.bench_function("cubic_position_Vec3A", |b| { b.iter(|| black_box(bezier.position(black_box(0.5)))); }); @@ -46,7 +48,8 @@ fn cubic_vec3(c: &mut Criterion) { vec3(1.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0), ]]) - .to_curve(); + .to_curve() + .expect("Unable to build a curve from this data"); c.bench_function("cubic_position_Vec3", |b| { b.iter(|| black_box(bezier.position(black_box(0.5)))); }); @@ -59,7 +62,8 @@ fn build_pos_cubic(c: &mut Criterion) { vec3a(1.0, 0.0, 0.0), vec3a(1.0, 1.0, 1.0), ]]) - .to_curve(); + .to_curve() + .expect("Unable to build a curve from this data"); c.bench_function("build_pos_cubic_100_points", |b| { b.iter(|| black_box(bezier.iter_positions(black_box(100)).collect::>())); }); @@ -72,7 +76,8 @@ fn build_accel_cubic(c: &mut Criterion) { vec3a(1.0, 0.0, 0.0), vec3a(1.0, 1.0, 1.0), ]]) - .to_curve(); + .to_curve() + .expect("Unable to build a curve from this data"); c.bench_function("build_accel_cubic_100_points", |b| { b.iter(|| black_box(bezier.iter_positions(black_box(100)).collect::>())); }); diff --git a/benches/benches/bevy_reflect/function.rs b/benches/benches/bevy_reflect/function.rs new file mode 100644 index 00000000000000..4eb97eee827b29 --- /dev/null +++ b/benches/benches/bevy_reflect/function.rs @@ -0,0 +1,62 @@ +use bevy_reflect::func::{ArgList, IntoFunction, TypedFunction}; +use bevy_reflect::prelude::*; +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; + +criterion_group!(benches, typed, into, call, clone); +criterion_main!(benches); + +fn add(a: i32, b: i32) -> i32 { + a + b +} + +fn typed(c: &mut Criterion) { + c.benchmark_group("typed") + .bench_function("function", |b| { + b.iter(|| add.get_function_info()); + }) + .bench_function("closure", |b| { + let capture = 25; + let closure = |a: i32| a + capture; + b.iter(|| closure.get_function_info()); + }); +} + +fn into(c: &mut Criterion) { + c.benchmark_group("into") + .bench_function("function", |b| { + b.iter(|| add.into_function()); + }) + .bench_function("closure", |b| { + let capture = 25; + let closure = |a: i32| a + capture; + b.iter(|| closure.into_function()); + }); +} + +fn call(c: &mut Criterion) { + c.benchmark_group("call") + .bench_function("function", |b| { + let add = add.into_function(); + b.iter_batched( + || ArgList::new().push_owned(75_i32).push_owned(25_i32), + |args| add.call(args), + BatchSize::SmallInput, + ); + }) + .bench_function("closure", |b| { + let capture = 25; + let add = (|a: i32| a + capture).into_function(); + b.iter_batched( + || ArgList::new().push_owned(75_i32), + |args| add.call(args), + BatchSize::SmallInput, + ); + }); +} + +fn clone(c: &mut Criterion) { + c.benchmark_group("clone").bench_function("function", |b| { + let add = add.into_function(); + b.iter(|| add.clone()); + }); +} diff --git a/benches/benches/bevy_reflect/struct.rs b/benches/benches/bevy_reflect/struct.rs index 0495701807c9ec..9c6163d24d3a31 100644 --- a/benches/benches/bevy_reflect/struct.rs +++ b/benches/benches/bevy_reflect/struct.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use bevy_reflect::{DynamicStruct, GetField, Reflect, Struct}; +use bevy_reflect::{DynamicStruct, GetField, PartialReflect, Reflect, Struct}; use criterion::{ black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput, }; @@ -62,7 +62,7 @@ fn concrete_struct_apply(criterion: &mut Criterion) { // Use functions that produce trait objects of varying concrete types as the // input to the benchmark. - let inputs: &[fn() -> (Box, Box)] = &[ + let inputs: &[fn() -> (Box, Box)] = &[ || (Box::new(Struct16::default()), Box::new(Struct16::default())), || (Box::new(Struct32::default()), Box::new(Struct32::default())), || (Box::new(Struct64::default()), Box::new(Struct64::default())), @@ -240,7 +240,7 @@ fn dynamic_struct_apply(criterion: &mut Criterion) { group.warm_up_time(WARM_UP_TIME); group.measurement_time(MEASUREMENT_TIME); - let patches: &[(fn() -> Box, usize)] = &[ + let patches: &[(fn() -> Box, usize)] = &[ (|| Box::new(Struct16::default()), 16), (|| Box::new(Struct32::default()), 32), (|| Box::new(Struct64::default()), 64), diff --git a/benches/benches/bevy_render/torus.rs b/benches/benches/bevy_render/torus.rs index 8ec81c80409d80..199cc7ce4c5eff 100644 --- a/benches/benches/bevy_render/torus.rs +++ b/benches/benches/bevy_render/torus.rs @@ -4,12 +4,9 @@ use bevy_render::mesh::TorusMeshBuilder; fn torus(c: &mut Criterion) { c.bench_function("build_torus", |b| { - b.iter(|| black_box(TorusMeshBuilder::new(black_box(0.5),black_box(1.0)))); + b.iter(|| black_box(TorusMeshBuilder::new(black_box(0.5), black_box(1.0)))); }); } -criterion_group!( - benches, - torus, -); +criterion_group!(benches, torus,); criterion_main!(benches); diff --git a/clippy.toml b/clippy.toml index faecf7d5af03e0..ccf511898b3900 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,7 +1,7 @@ doc-valid-idents = [ "GilRs", "glTF", - "MacOS", + "macOS", "NVidia", "OpenXR", "sRGB", diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 3dae29483cb7b7..62cf5ef240f353 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["bevy", "accessibility", "a11y"] bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } accesskit = "0.16" @@ -20,5 +21,5 @@ accesskit = "0.16" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index aab4c23f2c860d..92f514ab4513db 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -17,10 +17,11 @@ use accesskit::NodeBuilder; use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - prelude::{Component, Entity, Event}, + prelude::{Component, Entity, Event, ReflectResource}, schedule::SystemSet, system::Resource, }; +use bevy_reflect::Reflect; /// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`. #[derive(Event, Deref, DerefMut)] @@ -92,7 +93,8 @@ impl From for AccessibilityNode { } /// Resource representing which entity has keyboard focus, if any. -#[derive(Resource, Default, Deref, DerefMut)] +#[derive(Resource, Default, Deref, DerefMut, Reflect)] +#[reflect(Resource)] pub struct Focus(pub Option); /// Set enum for the systems relating to accessibility @@ -103,10 +105,13 @@ pub enum AccessibilitySystem { } /// Plugin managing non-GUI aspects of integrating with accessibility APIs. +#[derive(Default)] pub struct AccessibilityPlugin; impl Plugin for AccessibilityPlugin { fn build(&self, app: &mut bevy_app::App) { + app.register_type::(); + app.init_resource::() .init_resource::() .init_resource::() diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 6659311438e1f9..767a4e86758b8c 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -42,5 +42,5 @@ uuid = { version = "1.7", features = ["v4"] } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 7a703942aa6ac8..3f065b79111832 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -225,7 +225,7 @@ impl AnimationGraph { ) -> impl Iterator + 'a where I: IntoIterator>, - ::IntoIter: 'a, + ::IntoIter: 'a, { clips .into_iter() diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs old mode 100644 new mode 100755 index 30aebf1356ad63..e8e394b957df68 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -155,6 +155,29 @@ impl VariableCurve { Some(step_start) } + + /// Find the index of the keyframe at or before the current time. + /// + /// Returns the first keyframe if the `seek_time` is before the first keyframe, and + /// the second-to-last keyframe if the `seek_time` is after the last keyframe. + /// Panics if there are less than 2 keyframes. + pub fn find_interpolation_start_keyframe(&self, seek_time: f32) -> usize { + // 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()); + + // 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. + match search_result { + // An exact match was found + Ok(i) => i.clamp(0, self.keyframe_timestamps.len() - 2), + // No exact match was found, so return the previous keyframe to interpolate from. + Err(i) => (i.saturating_sub(1)).clamp(0, self.keyframe_timestamps.len() - 2), + } + } } /// Interpolation method to use between keyframes. @@ -564,14 +587,14 @@ thread_local! { impl AnimationPlayer { /// Start playing an animation, restarting it if necessary. pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { - self.active_animations.entry(animation).or_default() + let playing_animation = self.active_animations.entry(animation).or_default(); + playing_animation.replay(); + playing_animation } /// Start playing an animation, unless the requested animation is already playing. pub fn play(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation { - let playing_animation = self.active_animations.entry(animation).or_default(); - playing_animation.weight = 1.0; - playing_animation + self.active_animations.entry(animation).or_default() } /// Stops playing the given animation, removing it from the list of playing @@ -603,6 +626,7 @@ impl AnimationPlayer { self.active_animations.iter_mut() } + #[deprecated = "Use `animation_is_playing` instead"] /// Check if the given animation node is being played. pub fn is_playing_animation(&self, animation: AnimationNodeIndex) -> bool { self.active_animations.contains_key(&animation) @@ -874,18 +898,16 @@ impl AnimationTargetContext<'_> { // Some curves have only one keyframe used to set a transform if curve.keyframe_timestamps.len() == 1 { self.apply_single_keyframe(curve, weight); - return; + continue; } - // Find the current keyframe - let Some(step_start) = curve.find_current_keyframe(seek_time) else { - return; - }; + // Find the best keyframe to interpolate from + let step_start = curve.find_interpolation_start_keyframe(seek_time); 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, seek_time); + let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time).clamp(0.0, 1.0); self.apply_tweened_keyframe( curve, diff --git a/crates/bevy_animation/src/transition.rs b/crates/bevy_animation/src/transition.rs index 03c05f78c4531b..77e8fdfef5ca3e 100644 --- a/crates/bevy_animation/src/transition.rs +++ b/crates/bevy_animation/src/transition.rs @@ -5,9 +5,10 @@ use bevy_ecs::{ component::Component, + reflect::ReflectComponent, system::{Query, Res}, }; -use bevy_reflect::Reflect; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_time::Time; use bevy_utils::Duration; @@ -28,6 +29,7 @@ use crate::{graph::AnimationNodeIndex, ActiveAnimation, AnimationPlayer}; /// component to get confused about which animation is the "main" animation, and /// transitions will usually be incorrect as a result. #[derive(Component, Default, Reflect)] +#[reflect(Component, Default)] pub struct AnimationTransitions { main_animation: Option, transitions: Vec, @@ -90,7 +92,11 @@ impl AnimationTransitions { } } - self.main_animation = Some(new_animation); + // If already transitioning away from this animation, cancel the transition. + // Otherwise the transition ending would incorrectly stop the new animation. + self.transitions + .retain(|transition| transition.animation != new_animation); + player.start(new_animation) } diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 48affbf80e97f8..758654b6566953 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -13,6 +13,11 @@ trace = [] bevy_debug_stepping = [] default = ["bevy_reflect"] bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] +reflect_functions = [ + "bevy_reflect", + "bevy_reflect/functions", + "bevy_ecs/reflect_functions", +] [dependencies] # bevy @@ -38,5 +43,5 @@ console_error_panic_hook = "0.1.6" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 721bfe03eb26fe..f79bc6d52a55e9 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -96,6 +96,10 @@ impl Default for App { #[cfg(feature = "bevy_reflect")] app.init_resource::(); + + #[cfg(feature = "reflect_functions")] + app.init_resource::(); + app.add_plugins(MainSchedulePlugin); app.add_systems( First, @@ -553,7 +557,7 @@ impl App { self } - /// Registers the type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource, + /// Registers the type `T` in the [`AppTypeRegistry`] resource, /// adding reflect data as specified in the [`Reflect`](bevy_reflect::Reflect) derive: /// ```ignore (No serde "derive" feature) /// #[derive(Component, Serialize, Deserialize, Reflect)] @@ -567,7 +571,7 @@ impl App { self } - /// Associates type data `D` with type `T` in the [`TypeRegistry`](bevy_reflect::TypeRegistry) resource. + /// Associates type data `D` with type `T` in the [`AppTypeRegistry`] resource. /// /// Most of the time [`register_type`](Self::register_type) can be used instead to register a /// type you derived [`Reflect`](bevy_reflect::Reflect) for. However, in cases where you want to @@ -599,6 +603,156 @@ impl App { self } + /// Registers the given function into the [`AppFunctionRegistry`] resource. + /// + /// The given function will internally be stored as a [`DynamicFunction`] + /// and mapped according to its [name]. + /// + /// Because the function must have a name, + /// anonymous functions (e.g. `|a: i32, b: i32| { a + b }`) and closures must instead + /// be registered using [`register_function_with_name`] or converted to a [`DynamicFunction`] + /// and named using [`DynamicFunction::with_name`]. + /// Failure to do so will result in a panic. + /// + /// Only types that implement [`IntoFunction`] may be registered via this method. + /// + /// See [`FunctionRegistry::register`] for more information. + /// + /// # Panics + /// + /// Panics if a function has already been registered with the given name + /// or if the function is missing a name (such as when it is an anonymous function). + /// + /// # Examples + /// + /// ``` + /// use bevy_app::App; + /// + /// fn add(a: i32, b: i32) -> i32 { + /// a + b + /// } + /// + /// App::new().register_function(add); + /// ``` + /// + /// Functions cannot be registered more than once. + /// + /// ```should_panic + /// use bevy_app::App; + /// + /// fn add(a: i32, b: i32) -> i32 { + /// a + b + /// } + /// + /// App::new() + /// .register_function(add) + /// // Panic! A function has already been registered with the name "my_function" + /// .register_function(add); + /// ``` + /// + /// Anonymous functions and closures should be registered using [`register_function_with_name`] or given a name using [`DynamicFunction::with_name`]. + /// + /// ```should_panic + /// use bevy_app::App; + /// + /// // Panic! Anonymous functions cannot be registered using `register_function` + /// App::new().register_function(|a: i32, b: i32| a + b); + /// ``` + /// + /// [`register_function_with_name`]: Self::register_function_with_name + /// [`DynamicFunction`]: bevy_reflect::func::DynamicFunction + /// [name]: bevy_reflect::func::FunctionInfo::name + /// [`DynamicFunction::with_name`]: bevy_reflect::func::DynamicFunction::with_name + /// [`IntoFunction`]: bevy_reflect::func::IntoFunction + /// [`FunctionRegistry::register`]: bevy_reflect::func::FunctionRegistry::register + #[cfg(feature = "reflect_functions")] + pub fn register_function(&mut self, function: F) -> &mut Self + where + F: bevy_reflect::func::IntoFunction<'static, Marker> + 'static, + { + self.main_mut().register_function(function); + self + } + + /// Registers the given function or closure into the [`AppFunctionRegistry`] resource using the given name. + /// + /// To avoid conflicts, it's recommended to use a unique name for the function. + /// This can be achieved by "namespacing" the function with a unique identifier, + /// such as the name of your crate. + /// + /// For example, to register a function, `add`, from a crate, `my_crate`, + /// you could use the name, `"my_crate::add"`. + /// + /// Another approach could be to use the [type name] of the function, + /// however, it should be noted that anonymous functions do _not_ have unique type names. + /// + /// For named functions (e.g. `fn add(a: i32, b: i32) -> i32 { a + b }`) where a custom name is not needed, + /// it's recommended to use [`register_function`] instead as the generated name is guaranteed to be unique. + /// + /// Only types that implement [`IntoFunction`] may be registered via this method. + /// + /// See [`FunctionRegistry::register_with_name`] for more information. + /// + /// # Panics + /// + /// Panics if a function has already been registered with the given name. + /// + /// # Examples + /// + /// ``` + /// use bevy_app::App; + /// + /// fn mul(a: i32, b: i32) -> i32 { + /// a * b + /// } + /// + /// let div = |a: i32, b: i32| a / b; + /// + /// App::new() + /// // Registering an anonymous function with a unique name + /// .register_function_with_name("my_crate::add", |a: i32, b: i32| { + /// a + b + /// }) + /// // Registering an existing function with its type name + /// .register_function_with_name(std::any::type_name_of_val(&mul), mul) + /// // Registering an existing function with a custom name + /// .register_function_with_name("my_crate::mul", mul) + /// // Be careful not to register anonymous functions with their type name. + /// // This code works but registers the function with a non-unique name like `foo::bar::{{closure}}` + /// .register_function_with_name(std::any::type_name_of_val(&div), div); + /// ``` + /// + /// Names must be unique. + /// + /// ```should_panic + /// use bevy_app::App; + /// + /// fn one() {} + /// fn two() {} + /// + /// App::new() + /// .register_function_with_name("my_function", one) + /// // Panic! A function has already been registered with the name "my_function" + /// .register_function_with_name("my_function", two); + /// ``` + /// + /// [type name]: std::any::type_name + /// [`register_function`]: Self::register_function + /// [`IntoFunction`]: bevy_reflect::func::IntoFunction + /// [`FunctionRegistry::register_with_name`]: bevy_reflect::func::FunctionRegistry::register_with_name + #[cfg(feature = "reflect_functions")] + pub fn register_function_with_name( + &mut self, + name: impl Into>, + function: F, + ) -> &mut Self + where + F: bevy_reflect::func::IntoFunction<'static, Marker> + 'static, + { + self.main_mut().register_function_with_name(name, function); + self + } + /// Returns a reference to the [`World`]. pub fn world(&self) -> &World { self.main().world() @@ -920,7 +1074,7 @@ impl From for AppExit { } impl Termination for AppExit { - fn report(self) -> std::process::ExitCode { + fn report(self) -> ExitCode { match self { AppExit::Success => ExitCode::SUCCESS, // We leave logging an error to our users @@ -931,7 +1085,7 @@ impl Termination for AppExit { #[cfg(test)] mod tests { - use std::{iter, marker::PhantomData, mem, sync::Mutex}; + use std::{iter, marker::PhantomData, mem::size_of, sync::Mutex}; use bevy_ecs::{ change_detection::{DetectChanges, ResMut}, @@ -1257,7 +1411,7 @@ mod tests { fn app_exit_size() { // There wont be many of them so the size isn't a issue but // it's nice they're so small let's keep it that way. - assert_eq!(mem::size_of::(), mem::size_of::()); + assert_eq!(size_of::(), size_of::()); } #[test] diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 86bec47521e8d3..b389395d3dee76 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -18,7 +18,6 @@ mod sub_app; mod terminal_ctrl_c_handler; pub use app::*; -pub use bevy_derive::DynamicPlugin; pub use main_schedule::*; pub use panic_handler::*; pub use plugin::*; @@ -35,9 +34,10 @@ pub mod prelude { app::{App, AppExit}, main_schedule::{ First, FixedFirst, FixedLast, FixedPostUpdate, FixedPreUpdate, FixedUpdate, Last, Main, - PostStartup, PostUpdate, PreStartup, PreUpdate, SpawnScene, Startup, Update, + PostStartup, PostUpdate, PreStartup, PreUpdate, RunFixedMainLoop, + RunFixedMainLoopSystem, SpawnScene, Startup, Update, }, sub_app::SubApp, - DynamicPlugin, Plugin, PluginGroup, + Plugin, PluginGroup, }; } diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index cf1a3c9624aca2..3ee0a07a746d24 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -1,6 +1,9 @@ use crate::{App, Plugin}; use bevy_ecs::{ - schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel}, + schedule::{ + ExecutorKind, InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel, + SystemSet, + }, system::{Local, Resource}, world::{Mut, World}, }; @@ -75,6 +78,11 @@ pub struct First; pub struct PreUpdate; /// Runs the [`FixedMain`] schedule in a loop according until all relevant elapsed time has been "consumed". +/// If you need to order your variable timestep systems +/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set. +/// +/// Note that in contrast to most other Bevy schedules, systems added directly to +/// [`RunFixedMainLoop`] will *not* be parallelized between each other. /// /// See the [`Main`] schedule for some details about how schedules are run. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] @@ -94,8 +102,16 @@ pub struct FixedFirst; #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct FixedPreUpdate; -/// The schedule that contains most gameplay logic. +/// The schedule that contains most gameplay logic, which runs at a fixed rate rather than every render frame. +/// For logic that should run once per render frame, use the [`Update`] schedule instead. +/// +/// Examples of systems that should run at a fixed rate include (but are not limited to): +/// - Physics +/// - AI +/// - Networking +/// - Game rules /// +/// See the [`Update`] schedule for examples of systems that *should not* use this schedule. /// See the [`FixedMain`] schedule for details on how fixed updates work. /// See the [`Main`] schedule for some details about how schedules are run. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] @@ -118,8 +134,8 @@ pub struct FixedLast; /// The schedule that contains systems which only run after a fixed period of time has elapsed. /// -/// The exclusive `run_fixed_main_schedule` system runs this schedule. -/// This is run by the [`RunFixedMainLoop`] schedule. +/// This is run by the [`RunFixedMainLoop`] schedule. If you need to order your variable timestep systems +/// before or after the fixed update logic, use the [`RunFixedMainLoopSystem`] system set. /// /// Frequency of execution is configured by inserting `Time` resource, 64 Hz by default. /// See [this example](https://github.com/bevyengine/bevy/blob/latest/examples/time/time.rs). @@ -128,9 +144,15 @@ pub struct FixedLast; #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct FixedMain; -/// The schedule that contains app logic. Ideally containing anything that must run once per -/// render frame, such as UI. +/// The schedule that contains any app logic that must run once per render frame. +/// For most gameplay logic, consider using [`FixedUpdate`] instead. /// +/// Examples of systems that should run once per render frame include (but are not limited to): +/// - UI +/// - Input handling +/// - Audio control +/// +/// See the [`FixedUpdate`] schedule for examples of systems that *should not* use this schedule. /// See the [`Main`] schedule for some details about how schedules are run. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct Update; @@ -274,7 +296,16 @@ impl Plugin for MainSchedulePlugin { .init_resource::() .init_resource::() .add_systems(Main, Main::run_main) - .add_systems(FixedMain, FixedMain::run_fixed_main); + .add_systems(FixedMain, FixedMain::run_fixed_main) + .configure_sets( + RunFixedMainLoop, + ( + RunFixedMainLoopSystem::BeforeFixedMainLoop, + RunFixedMainLoopSystem::FixedMainLoop, + RunFixedMainLoopSystem::AfterFixedMainLoop, + ) + .chain(), + ); #[cfg(feature = "bevy_debug_stepping")] { @@ -338,3 +369,96 @@ impl FixedMain { }); } } + +/// Set enum for the systems that want to run inside [`RunFixedMainLoop`], +/// but before or after the fixed update logic. Systems in this set +/// will run exactly once per frame, regardless of the number of fixed updates. +/// They will also run under a variable timestep. +/// +/// This is useful for handling things that need to run every frame, but +/// also need to be read by the fixed update logic. See the individual variants +/// for examples of what kind of systems should be placed in each. +/// +/// Note that in contrast to most other Bevy schedules, systems added directly to +/// [`RunFixedMainLoop`] will *not* be parallelized between each other. +#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone, SystemSet)] +pub enum RunFixedMainLoopSystem { + /// Runs before the fixed update logic. + /// + /// A good example of a system that fits here + /// is camera movement, which needs to be updated in a variable timestep, + /// as you want the camera to move with as much precision and updates as + /// the frame rate allows. A physics system that needs to read the camera + /// position and orientation, however, should run in the fixed update logic, + /// as it needs to be deterministic and run at a fixed rate for better stability. + /// Note that we are not placing the camera movement system in `Update`, as that + /// would mean that the physics system already ran at that point. + /// + /// # Example + /// ``` + /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; + /// App::new() + /// .add_systems( + /// RunFixedMainLoop, + /// update_camera_rotation.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop)) + /// .add_systems(FixedUpdate, update_physics); + /// + /// # fn update_camera_rotation() {} + /// # fn update_physics() {} + /// ``` + BeforeFixedMainLoop, + /// Contains the fixed update logic. + /// Runs [`FixedMain`] zero or more times based on delta of + /// [`Time`] and [`Time::overstep`]. + /// + /// Don't place systems here, use [`FixedUpdate`] and friends instead. + /// Use this system instead to order your systems to run specifically inbetween the fixed update logic and all + /// other systems that run in [`RunFixedMainLoopSystem::BeforeFixedMainLoop`] or [`RunFixedMainLoopSystem::AfterFixedMainLoop`]. + /// + /// [`Time`]: https://docs.rs/bevy/latest/bevy/prelude/struct.Virtual.html + /// [`Time::overstep`]: https://docs.rs/bevy/latest/bevy/time/struct.Time.html#method.overstep + /// # Example + /// ``` + /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; + /// App::new() + /// .add_systems(FixedUpdate, update_physics) + /// .add_systems( + /// RunFixedMainLoop, + /// ( + /// // This system will be called before all interpolation systems + /// // that third-party plugins might add. + /// prepare_for_interpolation + /// .after(RunFixedMainLoopSystem::FixedMainLoop) + /// .before(RunFixedMainLoopSystem::AfterFixedMainLoop), + /// ) + /// ); + /// + /// # fn prepare_for_interpolation() {} + /// # fn update_physics() {} + /// ``` + FixedMainLoop, + /// Runs after the fixed update logic. + /// + /// A good example of a system that fits here + /// is a system that interpolates the transform of an entity between the last and current fixed update. + /// See the [fixed timestep example] for more details. + /// + /// [fixed timestep example]: https://github.com/bevyengine/bevy/blob/main/examples/movement/physics_in_fixed_timestep.rs + /// + /// # Example + /// ``` + /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; + /// App::new() + /// .add_systems(FixedUpdate, update_physics) + /// .add_systems( + /// RunFixedMainLoop, + /// interpolate_transforms.in_set(RunFixedMainLoopSystem::AfterFixedMainLoop)); + /// + /// # fn interpolate_transforms() {} + /// # fn update_physics() {} + /// ``` + AfterFixedMainLoop, +} diff --git a/crates/bevy_app/src/panic_handler.rs b/crates/bevy_app/src/panic_handler.rs index 89833a0140ddcb..94bb1c206cb761 100644 --- a/crates/bevy_app/src/panic_handler.rs +++ b/crates/bevy_app/src/panic_handler.rs @@ -1,5 +1,5 @@ //! This module provides panic handlers for [Bevy](https://bevyengine.org) -//! apps, and automatically configures platform specifics (i.e. WASM or Android). +//! apps, and automatically configures platform specifics (i.e. Wasm or Android). //! //! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`. //! @@ -11,7 +11,7 @@ use crate::Plugin; /// Adds sensible panic handlers to Apps. This plugin is part of the `DefaultPlugins`. Adding /// this plugin will setup a panic hook appropriate to your target platform: -/// * On WASM, uses [`console_error_panic_hook`](https://crates.io/crates/console_error_panic_hook), logging +/// * On Wasm, uses [`console_error_panic_hook`](https://crates.io/crates/console_error_panic_hook), logging /// to the browser console. /// * Other platforms are currently not setup. /// diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index fdaa579b2b7e35..006b645cbe99dd 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -120,16 +120,6 @@ impl Plugin for PlaceholderPlugin { fn build(&self, _app: &mut App) {} } -/// A type representing an unsafe function that returns a mutable pointer to a [`Plugin`]. -/// It is used for dynamically loading plugins. -/// -/// See `bevy_dynamic_plugin/src/loader.rs#dynamically_load_plugin`. -#[deprecated( - since = "0.14.0", - note = "The current dynamic plugin system is unsound and will be removed in 0.15." -)] -pub type CreatePlugin = unsafe fn() -> *mut dyn Plugin; - /// Types that represent a set of [`Plugin`]s. /// /// This is implemented for all types which implement [`Plugin`], diff --git a/crates/bevy_app/src/plugin_group.rs b/crates/bevy_app/src/plugin_group.rs index b41a78f067a1a0..0190ee762e36ab 100644 --- a/crates/bevy_app/src/plugin_group.rs +++ b/crates/bevy_app/src/plugin_group.rs @@ -2,7 +2,159 @@ use crate::{App, AppError, Plugin}; use bevy_utils::{tracing::debug, tracing::warn, TypeIdMap}; use std::any::TypeId; +/// A macro for generating a well-documented [`PluginGroup`] from a list of [`Plugin`] paths. +/// +/// Every plugin must implement the [`Default`] trait. +/// +/// # Example +/// +/// ``` +/// # use bevy_app::*; +/// # +/// # mod velocity { +/// # use bevy_app::*; +/// # #[derive(Default)] +/// # pub struct VelocityPlugin; +/// # impl Plugin for VelocityPlugin { fn build(&self, _: &mut App) {} } +/// # } +/// # +/// # mod collision { +/// # pub mod capsule { +/// # use bevy_app::*; +/// # #[derive(Default)] +/// # pub struct CapsuleCollisionPlugin; +/// # impl Plugin for CapsuleCollisionPlugin { fn build(&self, _: &mut App) {} } +/// # } +/// # } +/// # +/// # #[derive(Default)] +/// # pub struct TickratePlugin; +/// # impl Plugin for TickratePlugin { fn build(&self, _: &mut App) {} } +/// # +/// # mod features { +/// # use bevy_app::*; +/// # #[derive(Default)] +/// # pub struct ForcePlugin; +/// # impl Plugin for ForcePlugin { fn build(&self, _: &mut App) {} } +/// # } +/// # +/// # mod web { +/// # use bevy_app::*; +/// # #[derive(Default)] +/// # pub struct WebCompatibilityPlugin; +/// # impl Plugin for WebCompatibilityPlugin { fn build(&self, _: &mut App) {} } +/// # } +/// # +/// # mod internal { +/// # use bevy_app::*; +/// # #[derive(Default)] +/// # pub struct InternalPlugin; +/// # impl Plugin for InternalPlugin { fn build(&self, _: &mut App) {} } +/// # } +/// # +/// plugin_group! { +/// /// Doc comments and annotations are supported: they will be added to the generated plugin +/// /// group. +/// #[derive(Debug)] +/// pub struct PhysicsPlugins { +/// // If referencing a plugin within the same module, you must prefix it with a colon `:`. +/// :TickratePlugin, +/// // If referencing a plugin within a different module, there must be three colons `:::` +/// // between the final module and the plugin name. +/// collision::capsule:::CapsuleCollisionPlugin, +/// velocity:::VelocityPlugin, +/// // If you feature-flag a plugin, it will be automatically documented. There can only be +/// // one automatically documented feature flag, and it must be first. All other +/// // `#[cfg()]` attributes must be wrapped by `#[custom()]`. +/// #[cfg(feature = "external_forces")] +/// features:::ForcePlugin, +/// // More complicated `#[cfg()]`s and annotations are not supported by automatic doc +/// // generation, in which case you must wrap it in `#[custom()]`. +/// #[custom(cfg(target_arch = "wasm32"))] +/// web:::WebCompatibilityPlugin, +/// // You can hide plugins from documentation. Due to macro limitations, hidden plugins +/// // must be last. +/// #[doc(hidden)] +/// internal:::InternalPlugin +/// } +/// /// You may add doc comments after the plugin group as well. They will be appended after +/// /// the documented list of plugins. +/// } +/// ``` +#[macro_export] +macro_rules! plugin_group { + { + $(#[$group_meta:meta])* + $vis:vis struct $group:ident { + $( + $(#[cfg(feature = $plugin_feature:literal)])? + $(#[custom($plugin_meta:meta)])* + $($plugin_path:ident::)* : $plugin_name:ident + ),* + $( + $(,)?$( + #[doc(hidden)] + $(#[cfg(feature = $hidden_plugin_feature:literal)])? + $(#[custom($hidden_plugin_meta:meta)])* + $($hidden_plugin_path:ident::)* : $hidden_plugin_name:ident + ),+ + )? + + $(,)? + } + $($(#[doc = $post_doc:literal])+)? + } => { + $(#[$group_meta])* + /// + $(#[doc = concat!( + " - [`", stringify!($plugin_name), "`](" $(, stringify!($plugin_path), "::")*, stringify!($plugin_name), ")" + $(, " - with feature `", $plugin_feature, "`")? + )])* + $( + /// + $(#[doc = $post_doc])+ + )? + $vis struct $group; + + impl $crate::PluginGroup for $group { + fn build(self) -> $crate::PluginGroupBuilder { + let mut group = $crate::PluginGroupBuilder::start::(); + + $( + $(#[cfg(feature = $plugin_feature)])? + $(#[$plugin_meta])* + { + const _: () = { + const fn check_default() {} + check_default::<$($plugin_path::)*$plugin_name>(); + }; + + group = group.add(<$($plugin_path::)*$plugin_name>::default()); + } + )* + $($( + $(#[cfg(feature = $hidden_plugin_feature)])? + $(#[$hidden_plugin_meta])* + { + const _: () = { + const fn check_default() {} + check_default::<$($hidden_plugin_path::)*$hidden_plugin_name>(); + }; + + group = group.add(<$($hidden_plugin_path::)*$hidden_plugin_name>::default()); + } + )+)? + + group + } + } + }; +} + /// Combines multiple [`Plugin`]s into a single unit. +/// +/// If you want an easier, but slightly more restrictive, method of implementing this trait, you +/// may be interested in the [`plugin_group!`] macro. pub trait PluginGroup: Sized { /// Configures the [`Plugin`]s that are to be added. fn build(self) -> PluginGroupBuilder; @@ -27,6 +179,11 @@ impl PluginGroup for PluginGroupBuilder { } } +/// Helper method to get the [`TypeId`] of a value without having to name its type. +fn type_id_of_val(_: &T) -> TypeId { + TypeId::of::() +} + /// Facilitates the creation and configuration of a [`PluginGroup`]. /// Provides a build ordering to ensure that [`Plugin`]s which produce/require a [`Resource`](bevy_ecs::system::Resource) /// are built before/after dependent/depending [`Plugin`]s. [`Plugin`]s inside the group @@ -153,9 +310,9 @@ impl PluginGroupBuilder { /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] before the plugin of type `Target`. /// If the plugin was already the group, it is removed from its previous place. There must /// be a plugin of type `Target` in the group or it will panic. - pub fn add_before(mut self, plugin: T) -> Self { + pub fn add_before(mut self, plugin: impl Plugin) -> Self { let target_index = self.index_of::(); - self.order.insert(target_index, TypeId::of::()); + self.order.insert(target_index, type_id_of_val(&plugin)); self.upsert_plugin_state(plugin, target_index); self } @@ -163,9 +320,9 @@ impl PluginGroupBuilder { /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] after the plugin of type `Target`. /// If the plugin was already the group, it is removed from its previous place. There must /// be a plugin of type `Target` in the group or it will panic. - pub fn add_after(mut self, plugin: T) -> Self { + pub fn add_after(mut self, plugin: impl Plugin) -> Self { let target_index = self.index_of::() + 1; - self.order.insert(target_index, TypeId::of::()); + self.order.insert(target_index, type_id_of_val(&plugin)); self.upsert_plugin_state(plugin, target_index); self } @@ -285,7 +442,7 @@ mod tests { let group = PluginGroupBuilder::start::() .add(PluginA) .add(PluginB) - .add_after::(PluginC); + .add_after::(PluginC); assert_eq!( group.order, @@ -302,7 +459,7 @@ mod tests { let group = PluginGroupBuilder::start::() .add(PluginA) .add(PluginB) - .add_before::(PluginC); + .add_before::(PluginC); assert_eq!( group.order, @@ -338,7 +495,7 @@ mod tests { .add(PluginA) .add(PluginB) .add(PluginC) - .add_after::(PluginC); + .add_after::(PluginC); assert_eq!( group.order, @@ -356,7 +513,7 @@ mod tests { .add(PluginA) .add(PluginB) .add(PluginC) - .add_before::(PluginC); + .add_before::(PluginC); assert_eq!( group.order, diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 832f87357deb2a..48ffe9738830e9 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -408,6 +408,32 @@ impl SubApp { registry.write().register_type_data::(); self } + + /// See [`App::register_function`]. + #[cfg(feature = "reflect_functions")] + pub fn register_function(&mut self, function: F) -> &mut Self + where + F: bevy_reflect::func::IntoFunction<'static, Marker> + 'static, + { + let registry = self.world.resource_mut::(); + registry.write().register(function).unwrap(); + self + } + + /// See [`App::register_function_with_name`]. + #[cfg(feature = "reflect_functions")] + pub fn register_function_with_name( + &mut self, + name: impl Into>, + function: F, + ) -> &mut Self + where + F: bevy_reflect::func::IntoFunction<'static, Marker> + 'static, + { + let registry = self.world.resource_mut::(); + registry.write().register_with_name(name, function).unwrap(); + self + } } /// The collection of sub-apps that belong to an [`App`]. diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index c297ff1abce2b1..0b95ab505bae7e 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -67,5 +67,5 @@ bevy_log = { path = "../bevy_log", version = "0.15.0-dev" } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml index 7f62bfe338ab7b..a210d535299f66 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -22,5 +22,5 @@ quote = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index fe99875e0487e5..5a0dfcf824905b 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -193,10 +193,7 @@ impl DenseAssetStorage { Entry::None => return None, Entry::Some { value, generation } => { if *generation == index.generation { - value.take().map(|value| { - self.len -= 1; - value - }) + value.take().inspect(|_| self.len -= 1) } else { return None; } diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 24d01bc58315f4..95e480493a4c45 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -515,6 +515,8 @@ pub enum UntypedAssetConversionError { #[cfg(test)] mod tests { + use bevy_reflect::PartialReflect; + use super::*; type TestAsset = (); @@ -651,7 +653,7 @@ mod tests { ); let reflected: &dyn Reflect = &handle; - let cloned_handle: Box = reflected.clone_value(); + let cloned_handle: Box = reflected.clone_value(); assert_eq!( Arc::strong_count(strong), diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 66e0311b017a07..5ec179f846e50b 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_utils::tracing::error; +use bevy_utils::tracing::{debug, error}; #[cfg(feature = "file_watcher")] pub use file_watcher::*; @@ -41,6 +41,10 @@ impl FileAssetReader { /// See `get_base_path` below. pub fn new>(path: P) -> Self { let root_path = Self::get_base_path().join(path.as_ref()); + debug!( + "Asset Server using {} as its base path.", + root_path.display() + ); Self { root_path } } diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs index 302b7cd8391a84..2ac547e9b7136f 100644 --- a/crates/bevy_asset/src/io/file/sync_file_asset.rs +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -160,10 +160,7 @@ impl AssetReader for FileAssetReader { } } - async fn is_directory<'a>( - &'a self, - path: &'a Path, - ) -> std::result::Result { + async fn is_directory<'a>(&'a self, path: &'a Path) -> Result { let full_path = self.root_path.join(path); let metadata = full_path .metadata() @@ -194,35 +191,26 @@ impl AssetWriter for FileAssetWriter { Ok(writer) } - async fn remove<'a>(&'a self, path: &'a Path) -> std::result::Result<(), AssetWriterError> { + async fn remove<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { let full_path = self.root_path.join(path); std::fs::remove_file(full_path)?; Ok(()) } - async fn remove_meta<'a>( - &'a self, - path: &'a Path, - ) -> std::result::Result<(), AssetWriterError> { + async fn remove_meta<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { let meta_path = get_meta_path(path); let full_path = self.root_path.join(meta_path); std::fs::remove_file(full_path)?; Ok(()) } - async fn remove_directory<'a>( - &'a self, - path: &'a Path, - ) -> std::result::Result<(), AssetWriterError> { + async fn remove_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { let full_path = self.root_path.join(path); std::fs::remove_dir_all(full_path)?; Ok(()) } - async fn remove_empty_directory<'a>( - &'a self, - path: &'a Path, - ) -> std::result::Result<(), AssetWriterError> { + async fn remove_empty_directory<'a>(&'a self, path: &'a Path) -> Result<(), AssetWriterError> { let full_path = self.root_path.join(path); std::fs::remove_dir(full_path)?; Ok(()) @@ -231,7 +219,7 @@ impl AssetWriter for FileAssetWriter { async fn remove_assets_in_directory<'a>( &'a self, path: &'a Path, - ) -> std::result::Result<(), AssetWriterError> { + ) -> Result<(), AssetWriterError> { let full_path = self.root_path.join(path); std::fs::remove_dir_all(&full_path)?; std::fs::create_dir_all(&full_path)?; @@ -242,7 +230,7 @@ impl AssetWriter for FileAssetWriter { &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> std::result::Result<(), AssetWriterError> { + ) -> Result<(), AssetWriterError> { let full_old_path = self.root_path.join(old_path); let full_new_path = self.root_path.join(new_path); if let Some(parent) = full_new_path.parent() { @@ -256,7 +244,7 @@ impl AssetWriter for FileAssetWriter { &'a self, old_path: &'a Path, new_path: &'a Path, - ) -> std::result::Result<(), AssetWriterError> { + ) -> Result<(), AssetWriterError> { let old_meta_path = get_meta_path(old_path); let new_meta_path = get_meta_path(new_path); let full_old_path = self.root_path.join(old_meta_path); diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 5f1ae6cb8442ac..dbe559d5f2a85c 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -1,8 +1,8 @@ #[cfg(all(feature = "file_watcher", target_arch = "wasm32"))] compile_error!( "The \"file_watcher\" feature for hot reloading does not work \ - on WASM.\nDisable \"file_watcher\" \ - when compiling to WASM" + on Wasm.\nDisable \"file_watcher\" \ + when compiling to Wasm" ); #[cfg(target_os = "android")] @@ -24,13 +24,13 @@ pub use source::*; use bevy_utils::{BoxedFuture, ConditionalSendFuture}; use futures_io::{AsyncRead, AsyncSeek, AsyncWrite}; use futures_lite::{ready, Stream}; -use std::io::SeekFrom; -use std::task::Context; use std::{ + io::SeekFrom, + mem::size_of, path::{Path, PathBuf}, pin::Pin, sync::Arc, - task::Poll, + task::{Context, Poll}, }; use thiserror::Error; @@ -77,7 +77,7 @@ impl From for AssetReaderError { // Ideally this would be even smaller (ReadToEndFuture only needs space for two references based on its definition), // but compiler optimizations can apparently inflate the stack size of futures due to inlining, which makes // a higher maximum necessary. -pub const STACK_FUTURE_SIZE: usize = 10 * std::mem::size_of::<&()>(); +pub const STACK_FUTURE_SIZE: usize = 10 * size_of::<&()>(); pub use stackfuture::StackFuture; @@ -520,7 +520,7 @@ impl VecReader { impl AsyncRead for VecReader { fn poll_read( mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, + cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { if self.bytes_read >= self.bytes.len() { diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index 0c88cc63bf1a15..a77c7877970a8a 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -137,7 +137,7 @@ impl AsyncRead for TransactionLockedReader<'_> { mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>, buf: &mut [u8], - ) -> std::task::Poll> { + ) -> Poll> { Pin::new(&mut self.reader).poll_read(cx, buf) } } diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index 1ac6b65c22dd09..a979a3327791e7 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -70,9 +70,26 @@ impl<'a> AssetSourceId<'a> { } } -impl From<&'static str> for AssetSourceId<'static> { - fn from(value: &'static str) -> Self { - AssetSourceId::Name(value.into()) +impl AssetSourceId<'static> { + /// Indicates this [`AssetSourceId`] should have a static lifetime. + #[inline] + pub fn as_static(self) -> Self { + match self { + Self::Default => Self::Default, + Self::Name(value) => Self::Name(value.as_static()), + } + } + + /// Constructs an [`AssetSourceId`] with a static lifetime. + #[inline] + pub fn from_static(value: impl Into) -> Self { + value.into().as_static() + } +} + +impl<'a> From<&'a str> for AssetSourceId<'a> { + fn from(value: &'a str) -> Self { + AssetSourceId::Name(CowArc::Borrowed(value)) } } @@ -82,10 +99,10 @@ impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> { } } -impl From> for AssetSourceId<'static> { - fn from(value: Option<&'static str>) -> Self { +impl<'a> From> for AssetSourceId<'a> { + fn from(value: Option<&'a str>) -> Self { match value { - Some(value) => AssetSourceId::Name(value.into()), + Some(value) => AssetSourceId::Name(CowArc::Borrowed(value)), None => AssetSourceId::Default, } } @@ -302,7 +319,7 @@ pub struct AssetSourceBuilders { impl AssetSourceBuilders { /// Inserts a new builder with the given `id` pub fn insert(&mut self, id: impl Into>, source: AssetSourceBuilder) { - match id.into() { + match AssetSourceId::from_static(id) { AssetSourceId::Default => { self.default = Some(source); } diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 98163e43a884a9..6378556fe0e1c8 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -23,7 +23,7 @@ extern "C" { fn worker(this: &Global) -> JsValue; } -/// Reader implementation for loading assets via HTTP in WASM. +/// Reader implementation for loading assets via HTTP in Wasm. pub struct HttpWasmAssetReader { root_path: PathBuf, } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 5a1c4c7756d5d0..6c790b2b47d97b 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -341,7 +341,7 @@ impl AssetApp for App { id: impl Into>, source: AssetSourceBuilder, ) -> &mut Self { - let id = id.into(); + let id = AssetSourceId::from_static(id); if self.world().get_resource::().is_some() { error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id); } @@ -583,13 +583,10 @@ mod tests { async fn read_meta<'a>( &'a self, path: &'a Path, - ) -> Result { + ) -> Result { self.memory_reader.read_meta(path).await } - async fn read<'a>( - &'a self, - path: &'a Path, - ) -> Result { + async fn read<'a>(&'a self, path: &'a Path) -> Result { let attempt_number = { let mut attempt_counters = self.attempt_counters.lock().unwrap(); if let Some(existing) = attempt_counters.get_mut(path) { diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index bd7dda474ebade..1b444bba8c19b5 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -21,7 +21,7 @@ use thiserror::Error; /// should be loaded. pub trait AssetLoader: Send + Sync + 'static { /// The top level [`Asset`] loaded by this [`AssetLoader`]. - type Asset: crate::Asset; + type Asset: Asset; /// The settings type used by this [`AssetLoader`]. type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>; /// The type of [error](`std::error::Error`) which could be encountered by this loader. diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index 3c23fff5ad7122..dc7719a25f9ad6 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -475,14 +475,43 @@ impl<'a> AssetPath<'a> { } } -impl From<&'static str> for AssetPath<'static> { +impl AssetPath<'static> { + /// Indicates this [`AssetPath`] should have a static lifetime. #[inline] - fn from(asset_path: &'static str) -> Self { + pub fn as_static(self) -> Self { + let Self { + source, + path, + label, + } = self; + + let source = source.as_static(); + let path = path.as_static(); + let label = label.map(CowArc::as_static); + + Self { + source, + path, + label, + } + } + + /// Constructs an [`AssetPath`] with a static lifetime. + #[inline] + pub fn from_static(value: impl Into) -> Self { + value.into().as_static() + } +} + +impl<'a> From<&'a str> for AssetPath<'a> { + #[inline] + fn from(asset_path: &'a str) -> Self { let (source, path, label) = Self::parse_internal(asset_path).unwrap(); + AssetPath { source: source.into(), - path: CowArc::Static(path), - label: label.map(CowArc::Static), + path: CowArc::Borrowed(path), + label: label.map(CowArc::Borrowed), } } } @@ -501,12 +530,12 @@ impl From for AssetPath<'static> { } } -impl From<&'static Path> for AssetPath<'static> { +impl<'a> From<&'a Path> for AssetPath<'a> { #[inline] - fn from(path: &'static Path) -> Self { + fn from(path: &'a Path) -> Self { Self { source: AssetSourceId::Default, - path: CowArc::Static(path), + path: CowArc::Borrowed(path), label: None, } } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index f3da27d83f03b6..15acdbdc8d55d2 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -91,6 +91,10 @@ impl AssetProcessor { Self { server, data } } + pub fn data(&self) -> &Arc { + &self.data + } + /// The "internal" [`AssetServer`] used by the [`AssetProcessor`]. This is _separate_ from the asset processor used by /// the main App. It has different processor-specific configuration and a different ID space. pub fn server(&self) -> &AssetServer { @@ -153,7 +157,7 @@ impl AssetProcessor { /// Starts the processor in a background thread. pub fn start(_processor: Res) { #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] - error!("Cannot run AssetProcessor in single threaded mode (or WASM) yet."); + error!("Cannot run AssetProcessor in single threaded mode (or Wasm) yet."); #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let processor = _processor.clone(); @@ -323,7 +327,7 @@ impl AssetProcessor { AssetPath::from_path(&path).with_source(source.id()) ); #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] - error!("AddFolder event cannot be handled in single threaded mode (or WASM) yet."); + error!("AddFolder event cannot be handled in single threaded mode (or Wasm) yet."); #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] IoTaskPool::get().scope(|scope| { scope.spawn(async move { diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 7c36571c3e5378..a74a8b7fa5a0f4 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -1,7 +1,7 @@ use std::any::{Any, TypeId}; use bevy_ecs::world::{unsafe_world_cell::UnsafeWorldCell, World}; -use bevy_reflect::{FromReflect, FromType, Reflect}; +use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect}; use crate::{Asset, AssetId, Assets, Handle, UntypedAssetId, UntypedHandle}; @@ -22,8 +22,8 @@ pub struct ReflectAsset { // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably // - may only be used to access **at most one** access at once get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>, - add: fn(&mut World, &dyn Reflect) -> UntypedHandle, - insert: fn(&mut World, UntypedHandle, &dyn Reflect), + add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, + insert: fn(&mut World, UntypedHandle, &dyn PartialReflect), len: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, remove: fn(&mut World, UntypedHandle) -> Option>, @@ -94,11 +94,11 @@ impl ReflectAsset { } /// Equivalent of [`Assets::add`] - pub fn add(&self, world: &mut World, value: &dyn Reflect) -> UntypedHandle { + pub fn add(&self, world: &mut World, value: &dyn PartialReflect) -> UntypedHandle { (self.add)(world, value) } /// Equivalent of [`Assets::insert`] - pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn Reflect) { + pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn PartialReflect) { (self.insert)(world, handle, value); } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 10b5079de9b202..b2ea0466f68f02 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -22,13 +22,13 @@ use bevy_tasks::IoTaskPool; use bevy_utils::tracing::{error, info}; use bevy_utils::{CowArc, HashSet}; use crossbeam_channel::{Receiver, Sender}; -use futures_lite::StreamExt; +use futures_lite::{FutureExt, StreamExt}; use info::*; use loaders::*; use parking_lot::RwLock; -use std::future::Future; use std::{any::Any, path::PathBuf}; use std::{any::TypeId, path::Path, sync::Arc}; +use std::{future::Future, panic::AssertUnwindSafe}; use thiserror::Error; // Needed for doc string @@ -500,7 +500,7 @@ impl AssetServer { let (mut meta, loader, mut reader) = self .get_meta_loader_and_reader(&path_clone, asset_type_id) .await - .map_err(|e| { + .inspect_err(|e| { // 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 { @@ -510,7 +510,6 @@ impl AssetServer { error: e.clone(), }); } - e })?; // This contains Some(UntypedHandle), if it was retrievable @@ -726,7 +725,7 @@ impl AssetServer { .data .infos .write() - .create_loading_handle_untyped(std::any::TypeId::of::(), std::any::type_name::()); + .create_loading_handle_untyped(TypeId::of::(), std::any::type_name::()); let id = handle.id(); let event_sender = self.data.asset_event_sender.clone(); @@ -1177,13 +1176,20 @@ impl AssetServer { let asset_path = asset_path.clone_owned(); let load_context = LoadContext::new(self, asset_path.clone(), load_dependencies, populate_hashes); - loader.load(reader, meta, load_context).await.map_err(|e| { - AssetLoadError::AssetLoaderError(AssetLoaderError { + AssertUnwindSafe(loader.load(reader, meta, load_context)) + .catch_unwind() + .await + .map_err(|_| AssetLoadError::AssetLoaderPanic { path: asset_path.clone_owned(), loader_name: loader.type_name(), - error: e.into(), + })? + .map_err(|e| { + AssetLoadError::AssetLoaderError(AssetLoaderError { + path: asset_path.clone_owned(), + loader_name: loader.type_name(), + error: e.into(), + }) }) - }) } } @@ -1405,6 +1411,11 @@ pub enum AssetLoadError { CannotLoadProcessedAsset { path: AssetPath<'static> }, #[error("Asset '{path}' is configured to be ignored. It cannot be loaded.")] CannotLoadIgnoredAsset { path: AssetPath<'static> }, + #[error("Failed to load asset '{path}', asset loader '{loader_name}' panicked")] + AssetLoaderPanic { + path: AssetPath<'static>, + loader_name: &'static str, + }, #[error(transparent)] AssetLoaderError(#[from] AssetLoaderError), #[error(transparent)] diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index 810ea688cbb514..ff858e168b3d76 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -52,5 +52,5 @@ android_shared_stdcxx = ["cpal/oboe-shared-stdcxx"] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 35082ad398b04a..215cf712afb9a3 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -17,8 +17,8 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ bytemuck = { version = "1", features = ["derive"] } serde = { version = "1.0", features = ["derive"], optional = true } thiserror = "1.0" -wgpu-types = { version = "0.20", default-features = false, optional = true } -encase = { version = "0.8", default-features = false } +wgpu-types = { version = "22", default-features = false, optional = true } +encase = { version = "0.9", default-features = false } [features] default = ["bevy_reflect"] @@ -28,5 +28,5 @@ serialize = ["serde"] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index 32fdb83ba2ede4..ffba9467f61706 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -183,7 +183,7 @@ mod tests { #[test] fn test_gray() { - verify_gray::(); + verify_gray::(); verify_gray::(); verify_gray::(); verify_gray::(); diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index 3e6fefce055701..e708ccf67e5b07 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -157,7 +157,11 @@ impl From for Hsva { ) -> Self { // Based on https://en.wikipedia.org/wiki/HWB_color_model#Conversion let value = 1. - blackness; - let saturation = 1. - (whiteness / value); + let saturation = if value != 0. { + 1. - (whiteness / value) + } else { + 0. + }; Hsva::new(hue, saturation, value, alpha) } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index b1b63c91ac713e..e372ff492afcc0 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -118,7 +118,7 @@ impl Mix for Lcha { Self { lightness: self.lightness * n_factor + other.lightness * factor, chroma: self.chroma * n_factor + other.chroma * factor, - hue: self.hue * n_factor + other.hue * factor, + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), alpha: self.alpha * n_factor + other.alpha * factor, } } diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 08f3fac9081829..165fa0af95a243 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -114,7 +114,7 @@ impl Mix for Oklcha { Self { lightness: self.lightness * n_factor + other.lightness * factor, chroma: self.chroma * n_factor + other.chroma * factor, - hue: self.hue * n_factor + other.hue * factor, + hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor), alpha: self.alpha * n_factor + other.alpha * factor, } } diff --git a/crates/bevy_core/Cargo.toml b/crates/bevy_core/Cargo.toml index f94e33c8b31e3a..aaac27b0020ac2 100644 --- a/crates/bevy_core/Cargo.toml +++ b/crates/bevy_core/Cargo.toml @@ -39,5 +39,5 @@ serde_test = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index 1ef66f2345811c..df41f46c46df61 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -20,7 +20,8 @@ pub mod prelude { //! The Bevy Core Prelude. #[doc(hidden)] pub use crate::{ - DebugName, FrameCountPlugin, Name, TaskPoolOptions, TaskPoolPlugin, TypeRegistrationPlugin, + FrameCountPlugin, Name, NameOrEntity, TaskPoolOptions, TaskPoolPlugin, + TypeRegistrationPlugin, }; } diff --git a/crates/bevy_core/src/name.rs b/crates/bevy_core/src/name.rs index bf7c1acce0ad5e..9f858cf96e09cb 100644 --- a/crates/bevy_core/src/name.rs +++ b/crates/bevy_core/src/name.rs @@ -107,7 +107,7 @@ impl std::fmt::Debug for Name { /// # use bevy_core::prelude::*; /// # use bevy_ecs::prelude::*; /// # #[derive(Component)] pub struct Score(f32); -/// fn increment_score(mut scores: Query<(DebugName, &mut Score)>) { +/// fn increment_score(mut scores: Query<(NameOrEntity, &mut Score)>) { /// for (name, mut score) in &mut scores { /// score.0 += 1.0; /// if score.0.is_nan() { @@ -120,18 +120,18 @@ impl std::fmt::Debug for Name { /// /// # Implementation /// -/// The `Display` impl for `DebugName` returns the `Name` where there is one +/// The `Display` impl for `NameOrEntity` returns the `Name` where there is one /// or {index}v{generation} for entities without one. #[derive(QueryData)] #[query_data(derive(Debug))] -pub struct DebugName { +pub struct NameOrEntity { /// A [`Name`] that the entity might have that is displayed if available. pub name: Option<&'static Name>, /// The unique identifier of the entity as a fallback. pub entity: Entity, } -impl<'a> std::fmt::Display for DebugNameItem<'a> { +impl<'a> std::fmt::Display for NameOrEntityItem<'a> { #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self.name { @@ -227,12 +227,12 @@ mod tests { let e1 = world.spawn_empty().id(); let name = Name::new("MyName"); let e2 = world.spawn(name.clone()).id(); - let mut query = world.query::(); + let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); let d2 = query.get(&world, e2).unwrap(); - // DebugName Display for entities without a Name should be {index}v{generation} + // NameOrEntity Display for entities without a Name should be {index}v{generation} assert_eq!(d1.to_string(), "0v1"); - // DebugName Display for entities with a Name should be the Name + // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); } } diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index cac1c9f615c14b..9db01932f402fe 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -45,5 +45,5 @@ thiserror = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs index 6b648eeed48534..67dcead57a834b 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs @@ -42,6 +42,9 @@ pub struct AutoExposureCompensationCurve { /// Various errors that can occur when constructing an [`AutoExposureCompensationCurve`]. #[derive(Error, Debug)] pub enum AutoExposureCompensationCurveError { + /// The curve couldn't be built in the first place. + #[error("curve could not be constructed from the given data")] + InvalidCurve, /// A discontinuity was found in the curve. #[error("discontinuity found between curve segments")] DiscontinuityFound, @@ -99,7 +102,9 @@ impl AutoExposureCompensationCurve { where T: CubicGenerator, { - let curve = curve.to_curve(); + let Ok(curve) = curve.to_curve() else { + return Err(AutoExposureCompensationCurveError::InvalidCurve); + }; let min_log_lum = curve.position(0.0).x; let max_log_lum = curve.position(curve.segments().len() as f32).x; diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs index 148e74d0635768..d50d1451d839b3 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -110,7 +110,7 @@ impl FromWorld for AutoExposureResources { fn queue_view_auto_exposure_pipelines( mut commands: Commands, - pipeline_cache: ResMut, + pipeline_cache: Res, mut compute_pipelines: ResMut>, pipeline: Res, view_targets: Query<(Entity, &AutoExposureSettings)>, diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl index 8234b722136903..0b103331923826 100644 --- a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl +++ b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl @@ -10,6 +10,7 @@ struct BloomUniforms { threshold_precomputations: vec4, viewport: vec4, aspect: f32, + uv_offset: f32 }; @group(0) @binding(0) var input_texture: texture_2d; @@ -94,9 +95,9 @@ fn sample_input_13_tap(uv: vec2) -> vec3 { // [COD] slide 162 fn sample_input_3x3_tent(uv: vec2) -> vec3 { - // Radius. Empirically chosen by and tweaked from the LearnOpenGL article. - let x = 0.004 / uniforms.aspect; - let y = 0.004; + // UV offsets configured from uniforms. + let x = uniforms.uv_offset / uniforms.aspect; + let y = uniforms.uv_offset; let a = textureSample(input_texture, s, vec2(uv.x - x, uv.y + y)).rgb; let b = textureSample(input_texture, s, vec2(uv.x, uv.y + y)).rgb; diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 736ebeaf242881..ba48e4b0fe78ba 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -41,6 +41,7 @@ pub struct BloomUniforms { pub threshold_precomputations: Vec4, pub viewport: Vec4, pub aspect: f32, + pub uv_offset: f32, } impl FromWorld for BloomDownsamplingPipeline { diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 4069fd2cfcd901..b5c4c6b72c524e 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -38,10 +38,6 @@ const BLOOM_SHADER_HANDLE: Handle = Handle::weak_from_u128(9295994769239 const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Float; -// Maximum size of each dimension for the largest mipchain texture used in downscaling/upscaling. -// 512 behaves well with the UV offset of 0.004 used in bloom.wgsl -const MAX_MIP_DIMENSION: u32 = 512; - pub struct BloomPlugin; impl Plugin for BloomPlugin { @@ -328,17 +324,21 @@ fn prepare_bloom_textures( mut commands: Commands, mut texture_cache: ResMut, render_device: Res, - views: Query<(Entity, &ExtractedCamera), With>, + views: Query<(Entity, &ExtractedCamera, &BloomSettings)>, ) { - for (entity, camera) in &views { + for (entity, camera, settings) in &views { if let Some(UVec2 { x: width, y: height, }) = camera.physical_viewport_size { // How many times we can halve the resolution minus one so we don't go unnecessarily low - let mip_count = MAX_MIP_DIMENSION.ilog2().max(2) - 1; - let mip_height_ratio = MAX_MIP_DIMENSION as f32 / height as f32; + let mip_count = settings.max_mip_dimension.ilog2().max(2) - 1; + let mip_height_ratio = if height != 0 { + settings.max_mip_dimension as f32 / height as f32 + } else { + 0. + }; let texture_descriptor = TextureDescriptor { label: Some("bloom_texture"), diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 055f66c8d2d6a7..8fe66282144d13 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -102,9 +102,20 @@ pub struct BloomSettings { /// configured in a non-energy-conserving way, /// otherwise set to [`BloomCompositeMode::EnergyConserving`]. pub composite_mode: BloomCompositeMode, + + /// Maximum size of each dimension for the largest mipchain texture used in downscaling/upscaling. + /// Only tweak if you are seeing visual artifacts. + pub max_mip_dimension: u32, + + /// UV offset for bloom shader. Ideally close to 2.0 / `max_mip_dimension`. + /// Only tweak if you are seeing visual artifacts. + pub uv_offset: f32, } impl BloomSettings { + const DEFAULT_MAX_MIP_DIMENSION: u32 = 512; + const DEFAULT_UV_OFFSET: f32 = 0.004; + /// The default bloom preset. /// /// This uses the [`EnergyConserving`](BloomCompositeMode::EnergyConserving) composite mode. @@ -118,6 +129,8 @@ impl BloomSettings { threshold_softness: 0.0, }, composite_mode: BloomCompositeMode::EnergyConserving, + max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, + uv_offset: Self::DEFAULT_UV_OFFSET, }; /// A preset that's similar to how older games did bloom. @@ -131,6 +144,8 @@ impl BloomSettings { threshold_softness: 0.2, }, composite_mode: BloomCompositeMode::Additive, + max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, + uv_offset: Self::DEFAULT_UV_OFFSET, }; /// A preset that applies a very strong bloom, and blurs the whole screen. @@ -144,6 +159,8 @@ impl BloomSettings { threshold_softness: 0.0, }, composite_mode: BloomCompositeMode::EnergyConserving, + max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, + uv_offset: Self::DEFAULT_UV_OFFSET, }; } @@ -213,6 +230,7 @@ impl ExtractComponent for BloomSettings { / UVec4::new(target_size.x, target_size.y, target_size.x, target_size.y) .as_vec4(), aspect: AspectRatio::from_pixels(size.x, size.y).into(), + uv_offset: settings.uv_offset, }; Some((settings.clone(), uniform)) 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 5be54e87bc00fb..f76e06367231e7 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -23,7 +23,7 @@ use bevy_render::{ mod node; -pub use node::CASNode; +pub use node::CasNode; /// Applies a contrast adaptive sharpening (CAS) filter to the camera. /// @@ -66,28 +66,28 @@ impl Default for ContrastAdaptiveSharpeningSettings { #[derive(Component, Default, Reflect, Clone)] #[reflect(Component)] -pub struct DenoiseCAS(bool); +pub struct DenoiseCas(bool); /// The uniform struct extracted from [`ContrastAdaptiveSharpeningSettings`] attached to a [`Camera`]. /// Will be available for use in the CAS shader. #[doc(hidden)] #[derive(Component, ShaderType, Clone)] -pub struct CASUniform { +pub struct CasUniform { sharpness: f32, } impl ExtractComponent for ContrastAdaptiveSharpeningSettings { type QueryData = &'static Self; type QueryFilter = With; - type Out = (DenoiseCAS, CASUniform); + type Out = (DenoiseCas, CasUniform); fn extract_component(item: QueryItem) -> Option { if !item.enabled || item.sharpening_strength == 0.0 { return None; } Some(( - DenoiseCAS(item.denoise), - CASUniform { + DenoiseCas(item.denoise), + CasUniform { // above 1.0 causes extreme artifacts and fireflies sharpness: item.sharpening_strength.clamp(0.0, 1.0), }, @@ -99,9 +99,9 @@ const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle = Handle::weak_from_u128(6925381244141981602); /// Adds Support for Contrast Adaptive Sharpening (CAS). -pub struct CASPlugin; +pub struct CasPlugin; -impl Plugin for CASPlugin { +impl Plugin for CasPlugin { fn build(&self, app: &mut App) { load_internal_asset!( app, @@ -113,19 +113,19 @@ impl Plugin for CASPlugin { app.register_type::(); app.add_plugins(( ExtractComponentPlugin::::default(), - UniformComponentPlugin::::default(), + UniformComponentPlugin::::default(), )); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; render_app - .init_resource::>() + .init_resource::>() .add_systems(Render, prepare_cas_pipelines.in_set(RenderSet::Prepare)); { render_app - .add_render_graph_node::(Core3d, Node3d::ContrastAdaptiveSharpening) + .add_render_graph_node::(Core3d, Node3d::ContrastAdaptiveSharpening) .add_render_graph_edge( Core3d, Node3d::Tonemapping, @@ -142,7 +142,7 @@ impl Plugin for CASPlugin { } { render_app - .add_render_graph_node::(Core2d, Node2d::ContrastAdaptiveSharpening) + .add_render_graph_node::(Core2d, Node2d::ContrastAdaptiveSharpening) .add_render_graph_edge( Core2d, Node2d::Tonemapping, @@ -163,17 +163,17 @@ impl Plugin for CASPlugin { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app.init_resource::(); + render_app.init_resource::(); } } #[derive(Resource)] -pub struct CASPipeline { +pub struct CasPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, } -impl FromWorld for CASPipeline { +impl FromWorld for CasPipeline { fn from_world(render_world: &mut World) -> Self { let render_device = render_world.resource::(); let texture_bind_group = render_device.create_bind_group_layout( @@ -184,14 +184,14 @@ impl FromWorld for CASPipeline { texture_2d(TextureSampleType::Float { filterable: true }), sampler(SamplerBindingType::Filtering), // CAS Settings - uniform_buffer::(true), + uniform_buffer::(true), ), ), ); let sampler = render_device.create_sampler(&SamplerDescriptor::default()); - CASPipeline { + CasPipeline { texture_bind_group, sampler, } @@ -199,13 +199,13 @@ impl FromWorld for CASPipeline { } #[derive(PartialEq, Eq, Hash, Clone, Copy)] -pub struct CASPipelineKey { +pub struct CasPipelineKey { texture_format: TextureFormat, denoise: bool, } -impl SpecializedRenderPipeline for CASPipeline { - type Key = CASPipelineKey; +impl SpecializedRenderPipeline for CasPipeline { + type Key = CasPipelineKey; fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { let mut shader_defs = vec![]; @@ -237,15 +237,15 @@ impl SpecializedRenderPipeline for CASPipeline { fn prepare_cas_pipelines( mut commands: Commands, pipeline_cache: Res, - mut pipelines: ResMut>, - sharpening_pipeline: Res, - views: Query<(Entity, &ExtractedView, &DenoiseCAS), With>, + mut pipelines: ResMut>, + sharpening_pipeline: Res, + views: Query<(Entity, &ExtractedView, &DenoiseCas), With>, ) { for (entity, view, cas_settings) in &views { let pipeline_id = pipelines.specialize( &pipeline_cache, &sharpening_pipeline, - CASPipelineKey { + CasPipelineKey { denoise: cas_settings.0, texture_format: if view.hdr { ViewTarget::TEXTURE_FORMAT_HDR @@ -255,9 +255,9 @@ fn prepare_cas_pipelines( }, ); - commands.entity(entity).insert(ViewCASPipeline(pipeline_id)); + commands.entity(entity).insert(ViewCasPipeline(pipeline_id)); } } #[derive(Component)] -pub struct ViewCASPipeline(CachedRenderPipelineId); +pub struct ViewCasPipeline(CachedRenderPipelineId); diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs index 5c512747e54f0d..663d481e887bdf 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs @@ -1,6 +1,6 @@ use std::sync::Mutex; -use crate::contrast_adaptive_sharpening::ViewCASPipeline; +use crate::contrast_adaptive_sharpening::ViewCasPipeline; use bevy_ecs::prelude::*; use bevy_render::{ extract_component::{ComponentUniforms, DynamicUniformIndex}, @@ -13,21 +13,21 @@ use bevy_render::{ view::{ExtractedView, ViewTarget}, }; -use super::{CASPipeline, CASUniform}; +use super::{CasPipeline, CasUniform}; -pub struct CASNode { +pub struct CasNode { query: QueryState< ( &'static ViewTarget, - &'static ViewCASPipeline, - &'static DynamicUniformIndex, + &'static ViewCasPipeline, + &'static DynamicUniformIndex, ), With, >, cached_bind_group: Mutex>, } -impl FromWorld for CASNode { +impl FromWorld for CasNode { fn from_world(world: &mut World) -> Self { Self { query: QueryState::new(world), @@ -36,7 +36,7 @@ impl FromWorld for CASNode { } } -impl Node for CASNode { +impl Node for CasNode { fn update(&mut self, world: &mut World) { self.query.update_archetypes(world); } @@ -49,8 +49,8 @@ impl Node for CASNode { ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); let pipeline_cache = world.resource::(); - let sharpening_pipeline = world.resource::(); - let uniforms = world.resource::>(); + let sharpening_pipeline = world.resource::(); + let uniforms = world.resource::>(); let Ok((target, pipeline, uniform_index)) = self.query.get_manual(world, view_entity) else { 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 6230234bb53a6d..857c2202164ab5 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -2,6 +2,7 @@ use crate::core_2d::graph::Core2d; use crate::tonemapping::{DebandDither, Tonemapping}; use bevy_ecs::prelude::*; use bevy_reflect::Reflect; +use bevy_render::prelude::Msaa; use bevy_render::{ camera::{ Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph, @@ -35,6 +36,7 @@ pub struct Camera2dBundle { pub tonemapping: Tonemapping, pub deband_dither: DebandDither, pub main_texture_usages: CameraMainTextureUsages, + pub msaa: Msaa, } impl Default for Camera2dBundle { @@ -58,6 +60,7 @@ impl Default for Camera2dBundle { tonemapping: Tonemapping::None, deband_dither: DebandDither::Disabled, main_texture_usages: Default::default(), + msaa: Default::default(), } } } @@ -90,6 +93,7 @@ impl Camera2dBundle { tonemapping: Tonemapping::None, deband_dither: DebandDither::Disabled, main_texture_usages: Default::default(), + msaa: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs new file mode 100644 index 00000000000000..91093d0da5c944 --- /dev/null +++ b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs @@ -0,0 +1,105 @@ +use crate::core_2d::Opaque2d; +use bevy_ecs::{prelude::World, query::QueryItem}; +use bevy_render::{ + camera::ExtractedCamera, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, + render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, + renderer::RenderContext, + view::{ViewDepthTexture, ViewTarget}, +}; +use bevy_utils::tracing::error; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +use super::AlphaMask2d; + +/// A [`bevy_render::render_graph::Node`] that runs the +/// [`Opaque2d`] [`ViewBinnedRenderPhases`] and [`AlphaMask2d`] [`ViewBinnedRenderPhases`] +#[derive(Default)] +pub struct MainOpaquePass2dNode; +impl ViewNode for MainOpaquePass2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewDepthTexture, + ); + + fn run<'w>( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (camera, target, depth): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let (Some(opaque_phases), Some(alpha_mask_phases)) = ( + world.get_resource::>(), + world.get_resource::>(), + ) else { + return Ok(()); + }; + + let diagnostics = render_context.diagnostic_recorder(); + + let color_attachments = [Some(target.get_color_attachment())]; + let depth_stencil_attachment = Some(depth.get_attachment(StoreOp::Store)); + + let view_entity = graph.view_entity(); + let (Some(opaque_phase), Some(alpha_mask_phase)) = ( + opaque_phases.get(&view_entity), + alpha_mask_phases.get(&view_entity), + ) else { + return Ok(()); + }; + render_context.add_command_buffer_generation_task(move |render_device| { + #[cfg(feature = "trace")] + let _main_opaque_pass_2d_span = info_span!("main_opaque_pass_2d").entered(); + + // Command encoder setup + let mut command_encoder = + render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("main_opaque_pass_2d_command_encoder"), + }); + + // Render pass setup + let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("main_opaque_pass_2d"), + color_attachments: &color_attachments, + depth_stencil_attachment, + timestamp_writes: None, + occlusion_query_set: None, + }); + let mut render_pass = TrackedRenderPass::new(&render_device, render_pass); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_opaque_pass_2d"); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // Opaque draws + if !opaque_phase.is_empty() { + #[cfg(feature = "trace")] + let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered(); + if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the 2d opaque phase {err:?}"); + } + } + + // Alpha mask draws + if !alpha_mask_phase.is_empty() { + #[cfg(feature = "trace")] + let _alpha_mask_main_pass_2d_span = info_span!("alpha_mask_main_pass_2d").entered(); + if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the 2d alpha mask phase {err:?}"); + } + } + + pass_span.end(&mut render_pass); + drop(render_pass); + command_encoder.finish() + }); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index c5dafd45b7dff0..e365be954775bc 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -5,10 +5,11 @@ use bevy_render::{ diagnostic::RecordDiagnostics, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::ViewSortedRenderPhases, - render_resource::RenderPassDescriptor, + render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::ViewTarget, + view::{ViewDepthTexture, ViewTarget}, }; +use bevy_utils::tracing::error; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -16,13 +17,17 @@ use bevy_utils::tracing::info_span; pub struct MainTransparentPass2dNode {} impl ViewNode for MainTransparentPass2dNode { - type ViewQuery = (&'static ExtractedCamera, &'static ViewTarget); + type ViewQuery = ( + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewDepthTexture, + ); fn run<'w>( &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, target): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + (camera, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(transparent_phases) = @@ -46,7 +51,13 @@ impl ViewNode for MainTransparentPass2dNode { let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_transparent_pass_2d"), color_attachments: &[Some(target.get_color_attachment())], - depth_stencil_attachment: None, + // 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, }); @@ -58,7 +69,12 @@ impl ViewNode for MainTransparentPass2dNode { } if !transparent_phase.items.is_empty() { - transparent_phase.render(&mut render_pass, world, view_entity); + #[cfg(feature = "trace")] + let _transparent_main_pass_2d_span = + info_span!("transparent_main_pass_2d").entered(); + if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the transparent 2D phase {err:?}"); + } } pass_span.end(&mut render_pass); diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 869356c5023159..7bd6bea38cb587 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -1,4 +1,5 @@ mod camera_2d; +mod main_opaque_pass_2d_node; mod main_transparent_pass_2d_node; pub mod graph { @@ -15,9 +16,11 @@ pub mod graph { pub enum Node2d { MsaaWriteback, StartMainPass, + MainOpaquePass, MainTransparentPass, EndMainPass, Bloom, + PostProcessing, Tonemapping, Fxaa, Smaa, @@ -29,21 +32,31 @@ pub mod graph { use std::ops::Range; +use bevy_asset::UntypedAssetId; +use bevy_utils::HashMap; pub use camera_2d::*; +pub use main_opaque_pass_2d_node::*; pub use main_transparent_pass_2d_node::*; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_render::{ - camera::Camera, + camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, render_phase::{ - sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, - PhaseItemExtraIndex, SortedPhaseItem, ViewSortedRenderPhases, + sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, + DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases, + ViewSortedRenderPhases, }, - render_resource::CachedRenderPipelineId, + render_resource::{ + BindGroupId, CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension, + TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + texture::TextureCache, + view::{Msaa, ViewDepthTexture}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -51,6 +64,8 @@ use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; use self::graph::{Core2d, Node2d}; +pub const CORE_2D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + pub struct Core2dPlugin; impl Plugin for Core2dPlugin { @@ -62,17 +77,28 @@ impl Plugin for Core2dPlugin { return; }; render_app + .init_resource::>() + .init_resource::>() .init_resource::>() .init_resource::>() + .init_resource::>() + .init_resource::>() .add_systems(ExtractSchedule, extract_core_2d_camera_phases) .add_systems( Render, - sort_phase_system::.in_set(RenderSet::PhaseSort), + ( + sort_phase_system::.in_set(RenderSet::PhaseSort), + prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources), + ), ); render_app .add_render_sub_graph(Core2d) .add_render_graph_node::(Core2d, Node2d::StartMainPass) + .add_render_graph_node::>( + Core2d, + Node2d::MainOpaquePass, + ) .add_render_graph_node::>( Core2d, Node2d::MainTransparentPass, @@ -85,6 +111,7 @@ impl Plugin for Core2dPlugin { Core2d, ( Node2d::StartMainPass, + Node2d::MainOpaquePass, Node2d::MainTransparentPass, Node2d::EndMainPass, Node2d::Tonemapping, @@ -95,6 +122,177 @@ impl Plugin for Core2dPlugin { } } +/// Opaque 2D [`BinnedPhaseItem`]s. +pub struct Opaque2d { + /// The key, which determines which can be batched. + pub key: Opaque2dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: Entity, + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +/// Data that must be identical in order to batch phase items together. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Opaque2dBinKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The asset that this phase item is associated with. + /// + /// Normally, this is the ID of the mesh, but for non-mesh items it might be + /// the ID of another type of asset. + pub asset_id: UntypedAssetId, + /// The ID of a bind group specific to the material. + pub material_bind_group_id: Option, +} + +impl PhaseItem for Opaque2d { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for Opaque2d { + type BinKey = Opaque2dBinKey; + + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + Opaque2d { + key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for Opaque2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.key.pipeline + } +} + +/// Alpha mask 2D [`BinnedPhaseItem`]s. +pub struct AlphaMask2d { + /// The key, which determines which can be batched. + pub key: AlphaMask2dBinKey, + /// An entity from which data will be fetched, including the mesh if + /// applicable. + pub representative_entity: Entity, + /// The ranges of instances. + pub batch_range: Range, + /// An extra index, which is either a dynamic offset or an index in the + /// indirect parameters list. + pub extra_index: PhaseItemExtraIndex, +} + +/// Data that must be identical in order to batch phase items together. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AlphaMask2dBinKey { + /// The identifier of the render pipeline. + pub pipeline: CachedRenderPipelineId, + /// The function used to draw. + pub draw_function: DrawFunctionId, + /// The asset that this phase item is associated with. + /// + /// Normally, this is the ID of the mesh, but for non-mesh items it might be + /// the ID of another type of asset. + pub asset_id: UntypedAssetId, + /// The ID of a bind group specific to the material. + pub material_bind_group_id: Option, +} + +impl PhaseItem for AlphaMask2d { + #[inline] + fn entity(&self) -> Entity { + self.representative_entity + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.key.draw_function + } + + #[inline] + fn batch_range(&self) -> &Range { + &self.batch_range + } + + #[inline] + fn batch_range_mut(&mut self) -> &mut Range { + &mut self.batch_range + } + + fn extra_index(&self) -> PhaseItemExtraIndex { + self.extra_index + } + + fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range, &mut PhaseItemExtraIndex) { + (&mut self.batch_range, &mut self.extra_index) + } +} + +impl BinnedPhaseItem for AlphaMask2d { + type BinKey = AlphaMask2dBinKey; + + fn new( + key: Self::BinKey, + representative_entity: Entity, + batch_range: Range, + extra_index: PhaseItemExtraIndex, + ) -> Self { + AlphaMask2d { + key, + representative_entity, + batch_range, + extra_index, + } + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask2d { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.key.pipeline + } +} + +/// Transparent 2D [`SortedPhaseItem`]s. pub struct Transparent2d { pub sort_key: FloatOrd, pub entity: Entity, @@ -161,6 +359,8 @@ impl CachedRenderPipelinePhaseItem for Transparent2d { pub fn extract_core_2d_camera_phases( mut commands: Commands, mut transparent_2d_phases: ResMut>, + mut opaque_2d_phases: ResMut>, + mut alpha_mask_2d_phases: ResMut>, cameras_2d: Extract>>, mut live_entities: Local, ) { @@ -173,10 +373,63 @@ pub fn extract_core_2d_camera_phases( commands.get_or_spawn(entity); transparent_2d_phases.insert_or_clear(entity); + opaque_2d_phases.insert_or_clear(entity); + alpha_mask_2d_phases.insert_or_clear(entity); live_entities.insert(entity); } // Clear out all dead views. transparent_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); + opaque_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); + alpha_mask_2d_phases.retain(|camera_entity, _| live_entities.contains(camera_entity)); +} + +pub fn prepare_core_2d_depth_textures( + mut commands: Commands, + mut texture_cache: ResMut, + render_device: Res, + transparent_2d_phases: Res>, + opaque_2d_phases: Res>, + views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With,)>, +) { + let mut textures = HashMap::default(); + for (view, camera, msaa) in &views_2d { + if !opaque_2d_phases.contains_key(&view) || !transparent_2d_phases.contains_key(&view) { + continue; + }; + + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + // The size of the depth texture + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let descriptor = TextureDescriptor { + label: Some("view_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: CORE_2D_DEPTH_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + commands + .entity(view) + .insert(ViewDepthTexture::new(cached_texture, Some(0.0))); + } } 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 cf062d340ff799..697d9df64b3392 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -4,6 +4,7 @@ use crate::{ }; use bevy_ecs::prelude::*; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use bevy_render::view::Msaa; use bevy_render::{ camera::{Camera, CameraMainTextureUsages, CameraRenderGraph, Exposure, Projection}, extract_component::ExtractComponent, @@ -152,6 +153,7 @@ pub struct Camera3dBundle { pub color_grading: ColorGrading, pub exposure: Exposure, pub main_texture_usages: CameraMainTextureUsages, + pub msaa: Msaa, } // NOTE: ideally Perspective and Orthographic defaults can share the same impl, but sadly it breaks rust's type inference @@ -171,6 +173,7 @@ impl Default for Camera3dBundle { exposure: Default::default(), main_texture_usages: Default::default(), deband_dither: DebandDither::Enabled, + msaa: 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 dfd6fcc06feab6..b51f36354340ac 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 @@ -12,6 +12,7 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewTarget, ViewUniformOffset}, }; +use bevy_utils::tracing::error; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -95,14 +96,18 @@ impl ViewNode for MainOpaquePass3dNode { if !opaque_phase.is_empty() { #[cfg(feature = "trace")] let _opaque_main_pass_3d_span = info_span!("opaque_main_pass_3d").entered(); - opaque_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the opaque phase {err:?}"); + } } // Alpha draws if !alpha_mask_phase.is_empty() { #[cfg(feature = "trace")] let _alpha_mask_main_pass_3d_span = info_span!("alpha_mask_main_pass_3d").entered(); - alpha_mask_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = alpha_mask_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the alpha mask phase {err:?}"); + } } // Skybox draw using a fullscreen triangle 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 dadc4e07447938..f78083afb3816a 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 @@ -9,6 +9,7 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewTarget}, }; +use bevy_utils::tracing::error; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use std::ops::Range; @@ -98,7 +99,11 @@ impl ViewNode for MainTransmissivePass3dNode { } // render items in range - transmissive_phase.render_range(&mut render_pass, world, view_entity, range); + if let Err(err) = + transmissive_phase.render_range(&mut render_pass, world, view_entity, range) + { + error!("Error encountered while rendering the transmissive phase {err:?}"); + } } } else { let mut render_pass = @@ -108,7 +113,9 @@ impl ViewNode for MainTransmissivePass3dNode { render_pass.set_camera_viewport(viewport); } - transmissive_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = transmissive_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the transmissive phase {err:?}"); + } } } 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 4ab8a697e393d8..4f0d3d0722f0ec 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 @@ -9,6 +9,7 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewTarget}, }; +use bevy_utils::tracing::error; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -70,7 +71,9 @@ impl ViewNode for MainTransparentPass3dNode { render_pass.set_camera_viewport(viewport); } - transparent_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = transparent_phase.render(&mut render_pass, world, view_entity) { + error!("Error encountered while rendering the transparent phase {err:?}"); + } pass_span.end(&mut render_pass); } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index d2ddc769826685..bf38e1deb2159a 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -30,6 +30,7 @@ pub mod graph { Bloom, AutoExposure, DepthOfField, + PostProcessing, Tonemapping, Fxaa, Smaa, @@ -609,16 +610,21 @@ pub fn extract_camera_prepass_phase( pub fn prepare_core_3d_depth_textures( mut commands: Commands, mut texture_cache: ResMut, - msaa: Res, render_device: Res, opaque_3d_phases: Res>, alpha_mask_3d_phases: Res>, transmissive_3d_phases: Res>, transparent_3d_phases: Res>, - views_3d: Query<(Entity, &ExtractedCamera, Option<&DepthPrepass>, &Camera3d)>, + views_3d: Query<( + Entity, + &ExtractedCamera, + Option<&DepthPrepass>, + &Camera3d, + &Msaa, + )>, ) { let mut render_target_usage = HashMap::default(); - for (view, camera, depth_prepass, camera_3d) in &views_3d { + for (view, camera, depth_prepass, camera_3d, _msaa) in &views_3d { if !opaque_3d_phases.contains_key(&view) || !alpha_mask_3d_phases.contains_key(&view) || !transmissive_3d_phases.contains_key(&view) @@ -640,13 +646,13 @@ pub fn prepare_core_3d_depth_textures( } let mut textures = HashMap::default(); - for (entity, camera, _, camera_3d) in &views_3d { + for (entity, camera, _, camera_3d, msaa) in &views_3d { let Some(physical_target_size) = camera.physical_target_size else { continue; }; let cached_texture = textures - .entry(camera.target.clone()) + .entry((camera.target.clone(), msaa)) .or_insert_with(|| { // The size of the depth texture let size = Extent3d { @@ -778,11 +784,8 @@ pub fn prepare_core_3d_transmission_textures( } // Disable MSAA and warn if using deferred rendering -pub fn check_msaa( - mut msaa: ResMut, - deferred_views: Query, With)>, -) { - if !deferred_views.is_empty() { +pub fn check_msaa(mut deferred_views: Query<&mut Msaa, (With, With)>) { + for mut msaa in deferred_views.iter_mut() { match *msaa { Msaa::Off => (), _ => { @@ -798,7 +801,6 @@ pub fn check_msaa( pub fn prepare_prepass_textures( mut commands: Commands, mut texture_cache: ResMut, - msaa: Res, render_device: Res, opaque_3d_prepass_phases: Res>, alpha_mask_3d_prepass_phases: Res>, @@ -807,6 +809,7 @@ pub fn prepare_prepass_textures( views_3d: Query<( Entity, &ExtractedCamera, + &Msaa, Has, Has, Has, @@ -818,8 +821,15 @@ pub fn prepare_prepass_textures( let mut deferred_textures = HashMap::default(); let mut deferred_lighting_id_textures = HashMap::default(); let mut motion_vectors_textures = HashMap::default(); - for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in - &views_3d + for ( + entity, + camera, + msaa, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + ) in &views_3d { if !opaque_3d_prepass_phases.contains_key(&entity) && !alpha_mask_3d_prepass_phases.contains_key(&entity) diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index 21df5f4ed9f6a8..44895aaeb8cd24 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -11,6 +11,7 @@ use bevy_render::{ renderer::RenderContext, view::ViewDepthTexture, }; +use bevy_utils::tracing::error; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -149,14 +150,23 @@ impl ViewNode for DeferredGBufferPrepassNode { { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered(); - opaque_deferred_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) + { + error!("Error encountered while rendering the opaque deferred phase {err:?}"); + } } // Alpha masked draws if !alpha_mask_deferred_phase.is_empty() { #[cfg(feature = "trace")] let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered(); - alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = + alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity) + { + error!( + "Error encountered while rendering the alpha mask deferred phase {err:?}" + ); + } } drop(render_pass); diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 032e9f63d703ce..a35bdc0723d8a4 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -21,10 +21,12 @@ use bevy_ecs::{ component::Component, entity::Entity, query::{QueryItem, With}, + reflect::ReflectComponent, schedule::IntoSystemConfigs as _, system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::{ camera::{PhysicalCameraParameters, Projection}, extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, @@ -67,7 +69,8 @@ const DOF_SHADER_HANDLE: Handle = Handle::weak_from_u128(203186118073921 pub struct DepthOfFieldPlugin; /// Depth of field settings. -#[derive(Component, Clone, Copy)] +#[derive(Component, Clone, Copy, Reflect)] +#[reflect(Component, Default)] pub struct DepthOfFieldSettings { /// The appearance of the effect. pub mode: DepthOfFieldMode, @@ -110,7 +113,8 @@ pub struct DepthOfFieldSettings { } /// Controls the appearance of the effect. -#[derive(Component, Clone, Copy, Default, PartialEq, Debug)] +#[derive(Clone, Copy, Default, PartialEq, Debug, Reflect)] +#[reflect(Default, PartialEq)] pub enum DepthOfFieldMode { /// A more accurate simulation, in which circles of confusion generate /// "spots" of light. @@ -195,6 +199,8 @@ impl Plugin for DepthOfFieldPlugin { fn build(&self, app: &mut App) { load_internal_asset!(app, DOF_SHADER_HANDLE, "dof.wgsl", Shader::from_wgsl); + app.register_type::(); + app.register_type::(); app.add_plugins(UniformComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { @@ -515,11 +521,10 @@ impl FromWorld for DepthOfFieldGlobalBindGroupLayout { /// specific to each view. pub fn prepare_depth_of_field_view_bind_group_layouts( mut commands: Commands, - view_targets: Query<(Entity, &DepthOfFieldSettings)>, - msaa: Res, + view_targets: Query<(Entity, &DepthOfFieldSettings, &Msaa)>, render_device: Res, ) { - for (view, dof_settings) in view_targets.iter() { + for (view, dof_settings, msaa) in view_targets.iter() { // Create the bind group layout for the passes that take one input. let single_input = render_device.create_bind_group_layout( Some("depth of field bind group layout (single input)"), @@ -646,16 +651,16 @@ pub fn prepare_depth_of_field_pipelines( mut commands: Commands, pipeline_cache: Res, mut pipelines: ResMut>, - msaa: Res, global_bind_group_layout: Res, view_targets: Query<( Entity, &ExtractedView, &DepthOfFieldSettings, &ViewDepthOfFieldBindGroupLayouts, + &Msaa, )>, ) { - for (entity, view, dof_settings, view_bind_group_layouts) in view_targets.iter() { + for (entity, view, dof_settings, view_bind_group_layouts, msaa) in view_targets.iter() { let dof_pipeline = DepthOfFieldPipeline { view_bind_group_layouts: view_bind_group_layouts.clone(), global_bind_group_layout: global_bind_group_layout.layout.clone(), diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index fec38e432b75c1..568f2cf3f18241 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -19,6 +19,7 @@ pub mod fullscreen_vertex_shader; pub mod fxaa; pub mod motion_blur; pub mod msaa_writeback; +pub mod post_process; pub mod prepass; mod skybox; pub mod smaa; @@ -51,7 +52,7 @@ pub mod prelude { use crate::{ blit::BlitPlugin, bloom::BloomPlugin, - contrast_adaptive_sharpening::CASPlugin, + contrast_adaptive_sharpening::CasPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin, deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, @@ -60,6 +61,7 @@ use crate::{ fxaa::FxaaPlugin, motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, + post_process::PostProcessingPlugin, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, smaa::SmaaPlugin, tonemapping::TonemappingPlugin, @@ -95,10 +97,11 @@ impl Plugin for CorePipelinePlugin { UpscalingPlugin, BloomPlugin, FxaaPlugin, - CASPlugin, + CasPlugin, MotionBlurPlugin, DepthOfFieldPlugin, SmaaPlugin, + PostProcessingPlugin, )); } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/node.rs b/crates/bevy_core_pipeline/src/motion_blur/node.rs index 395314892bb518..24f470c563742f 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/node.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/node.rs @@ -27,12 +27,13 @@ impl ViewNode for MotionBlurNode { &'static MotionBlurPipelineId, &'static ViewPrepassTextures, &'static MotionBlur, + &'static Msaa, ); fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (view_target, pipeline_id, prepass_textures, settings): QueryItem, + (view_target, pipeline_id, prepass_textures, settings, msaa): QueryItem, world: &World, ) -> Result<(), NodeRunError> { if settings.samples == 0 || settings.shutter_angle <= 0.0 { @@ -60,7 +61,6 @@ impl ViewNode for MotionBlurNode { let post_process = view_target.post_process_write(); - let msaa = world.resource::(); let layout = if msaa.samples() == 1 { &motion_blur_pipeline.layout } else { diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index 7ad94d8874253f..cff26a9e22f170 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -153,10 +153,9 @@ pub(crate) fn prepare_motion_blur_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, pipeline: Res, - msaa: Res, - views: Query<(Entity, &ExtractedView), With>, + views: Query<(Entity, &ExtractedView, &Msaa), With>, ) { - for (entity, view) in &views { + for (entity, view, msaa) in &views { let pipeline_id = pipelines.specialize( &pipeline_cache, &pipeline, diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 5165f51ab08da8..01f579a0e1404a 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -6,9 +6,11 @@ use crate::{ use bevy_app::{App, Plugin}; use bevy_color::LinearRgba; use bevy_ecs::prelude::*; +use bevy_ecs::query::QueryItem; +use bevy_render::render_graph::{ViewNode, ViewNodeRunner}; use bevy_render::{ camera::ExtractedCamera, - render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, + render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext}, renderer::RenderContext, view::{Msaa, ViewTarget}, Render, RenderSet, @@ -30,90 +32,87 @@ impl Plugin for MsaaWritebackPlugin { ); { render_app - .add_render_graph_node::(Core2d, Node2d::MsaaWriteback) + .add_render_graph_node::>( + Core2d, + Node2d::MsaaWriteback, + ) .add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::StartMainPass); } { render_app - .add_render_graph_node::(Core3d, Node3d::MsaaWriteback) + .add_render_graph_node::>( + Core3d, + Node3d::MsaaWriteback, + ) .add_render_graph_edge(Core3d, Node3d::MsaaWriteback, Node3d::StartMainPass); } } } -pub struct MsaaWritebackNode { - cameras: QueryState<(&'static ViewTarget, &'static MsaaWritebackBlitPipeline)>, -} +#[derive(Default)] +pub struct MsaaWritebackNode; -impl FromWorld for MsaaWritebackNode { - fn from_world(world: &mut World) -> Self { - Self { - cameras: world.query(), - } - } -} +impl ViewNode for MsaaWritebackNode { + type ViewQuery = ( + &'static ViewTarget, + &'static MsaaWritebackBlitPipeline, + &'static Msaa, + ); -impl Node for MsaaWritebackNode { - fn update(&mut self, world: &mut World) { - self.cameras.update_archetypes(world); - } - - fn run( + fn run<'w>( &self, - graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (target, blit_pipeline_id, msaa): QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError> { - if *world.resource::() == Msaa::Off { + if *msaa == 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::(); - let pipeline_cache = world.resource::(); - let Some(pipeline) = pipeline_cache.get_render_pipeline(blit_pipeline_id.0) else { - return Ok(()); - }; - - // The current "main texture" needs to be bound as an input resource, and we need the "other" - // unused target to be the "resolve target" for the MSAA write. Therefore this is the same - // as a post process write! - let post_process = target.post_process_write(); + let blit_pipeline = world.resource::(); + let pipeline_cache = world.resource::(); + let Some(pipeline) = pipeline_cache.get_render_pipeline(blit_pipeline_id.0) else { + return Ok(()); + }; - let pass_descriptor = RenderPassDescriptor { - label: Some("msaa_writeback"), - // 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(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(LinearRgba::BLACK.into()), - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }; + // The current "main texture" needs to be bound as an input resource, and we need the "other" + // unused target to be the "resolve target" for the MSAA write. Therefore this is the same + // as a post process write! + let post_process = target.post_process_write(); + + let pass_descriptor = RenderPassDescriptor { + label: Some("msaa_writeback"), + // 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(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(LinearRgba::BLACK.into()), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; - let bind_group = render_context.render_device().create_bind_group( - None, - &blit_pipeline.texture_bind_group, - &BindGroupEntries::sequential((post_process.source, &blit_pipeline.sampler)), - ); + let bind_group = render_context.render_device().create_bind_group( + None, + &blit_pipeline.texture_bind_group, + &BindGroupEntries::sequential((post_process.source, &blit_pipeline.sampler)), + ); - let mut render_pass = render_context - .command_encoder() - .begin_render_pass(&pass_descriptor); + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); - render_pass.set_pipeline(pipeline); - render_pass.set_bind_group(0, &bind_group, &[]); - render_pass.draw(0..3, 0..1); - } + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[]); + render_pass.draw(0..3, 0..1); Ok(()) } @@ -127,10 +126,9 @@ fn prepare_msaa_writeback_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, blit_pipeline: Res, - view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera)>, - msaa: Res, + view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera, &Msaa)>, ) { - for (entity, view_target, camera) in view_targets.iter() { + for (entity, view_target, camera, msaa) in view_targets.iter() { // only do writeback if writeback is enabled for the camera and this isn't the first camera in the target, // as there is nothing to write back for the first camera. if msaa.samples() > 1 && camera.msaa_writeback && camera.sorted_camera_index_for_target > 0 diff --git a/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl b/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl new file mode 100644 index 00000000000000..905b11d7e688bd --- /dev/null +++ b/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl @@ -0,0 +1,92 @@ +// The chromatic aberration postprocessing effect. +// +// This makes edges of objects turn into multicolored streaks. + +#define_import_path bevy_core_pipeline::post_processing::chromatic_aberration + +// See `bevy_core_pipeline::post_process::ChromaticAberration` for more +// information on these fields. +struct ChromaticAberrationSettings { + intensity: f32, + max_samples: u32, + unused_a: u32, + unused_b: u32, +} + +// The source framebuffer texture. +@group(0) @binding(0) var chromatic_aberration_source_texture: texture_2d; +// The sampler used to sample the source framebuffer texture. +@group(0) @binding(1) var chromatic_aberration_source_sampler: sampler; +// The 1D lookup table for chromatic aberration. +@group(0) @binding(2) var chromatic_aberration_lut_texture: texture_2d; +// The sampler used to sample that lookup table. +@group(0) @binding(3) var chromatic_aberration_lut_sampler: sampler; +// The settings supplied by the developer. +@group(0) @binding(4) var chromatic_aberration_settings: ChromaticAberrationSettings; + +fn chromatic_aberration(start_pos: vec2) -> vec3 { + // Radial chromatic aberration implemented using the *Inside* technique: + // + // + + let end_pos = mix(start_pos, vec2(0.5), chromatic_aberration_settings.intensity); + + // Determine the number of samples. We aim for one sample per texel, unless + // that's higher than the developer-specified maximum number of samples, in + // which case we choose the maximum number of samples. + let texel_length = length((end_pos - start_pos) * + vec2(textureDimensions(chromatic_aberration_source_texture))); + let sample_count = min(u32(ceil(texel_length)), chromatic_aberration_settings.max_samples); + + var color: vec3; + if (sample_count > 1u) { + // The LUT texture is in clamp-to-edge mode, so we start at 0.5 texels + // from the sides so that we have a nice gradient over the entire LUT + // range. + let lut_u_offset = 0.5 / f32(textureDimensions(chromatic_aberration_lut_texture).x); + + var sample_sum = vec3(0.0); + var modulate_sum = vec3(0.0); + + // Start accumulating samples. + for (var sample_index = 0u; sample_index < sample_count; sample_index += 1u) { + let t = (f32(sample_index) + 0.5) / f32(sample_count); + + // Sample the framebuffer. + let sample_uv = mix(start_pos, end_pos, t); + let sample = textureSampleLevel( + chromatic_aberration_source_texture, + chromatic_aberration_source_sampler, + sample_uv, + 0.0, + ).rgb; + + // Sample the LUT. + let lut_u = mix(lut_u_offset, 1.0 - lut_u_offset, t); + let modulate = textureSampleLevel( + chromatic_aberration_lut_texture, + chromatic_aberration_lut_sampler, + vec2(lut_u, 0.5), + 0.0, + ).rgb; + + // Modulate the sample by the LUT value. + sample_sum += sample * modulate; + modulate_sum += modulate; + } + + color = sample_sum / modulate_sum; + } else { + // If there's only one sample, don't do anything. If we don't do this, + // then this shader will apply whatever tint is in the center of the LUT + // texture to such pixels, which is wrong. + color = textureSampleLevel( + chromatic_aberration_source_texture, + chromatic_aberration_source_sampler, + start_pos, + 0.0, + ).rgb; + } + + return color; +} diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs new file mode 100644 index 00000000000000..79c41f990b33ce --- /dev/null +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -0,0 +1,505 @@ +//! Miscellaneous built-in postprocessing effects. +//! +//! Currently, this consists only of chromatic aberration. + +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Assets, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryItem, With}, + reflect::ReflectComponent, + schedule::IntoSystemConfigs as _, + system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + camera::Camera, + extract_component::{ExtractComponent, ExtractComponentPlugin}, + render_asset::{RenderAssetUsages, RenderAssets}, + render_graph::{ + NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, + ColorTargetState, ColorWrites, DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, + Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, + ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, + TextureDimension, TextureFormat, TextureSampleType, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::{BevyDefault, GpuImage, Image}, + view::{ExtractedView, ViewTarget}, + Render, RenderApp, RenderSet, +}; +use bevy_utils::prelude::default; + +use crate::{ + core_2d::graph::{Core2d, Node2d}, + core_3d::graph::{Core3d, Node3d}, + fullscreen_vertex_shader, +}; + +/// The handle to the built-in postprocessing shader `post_process.wgsl`. +const POST_PROCESSING_SHADER_HANDLE: Handle = Handle::weak_from_u128(14675654334038973533); +/// The handle to the chromatic aberration shader `chromatic_aberration.wgsl`. +const CHROMATIC_ABERRATION_SHADER_HANDLE: Handle = + Handle::weak_from_u128(10969893303667163833); + +/// The handle to the default chromatic aberration lookup texture. +/// +/// This is just a 3x1 image consisting of one red pixel, one green pixel, and +/// one blue pixel, in that order. +const DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE: Handle = + Handle::weak_from_u128(2199972955136579180); + +/// The default chromatic aberration intensity amount, in a fraction of the +/// window size. +const DEFAULT_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.02; + +/// The default maximum number of samples for chromatic aberration. +const DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES: u32 = 8; + +/// The raw RGBA data for the default chromatic aberration gradient. +/// +/// This consists of one red pixel, one green pixel, and one blue pixel, in that +/// order. +static DEFAULT_CHROMATIC_ABERRATION_LUT_DATA: [u8; 12] = + [255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255]; + +/// A plugin that implements a built-in postprocessing stack with some common +/// effects. +/// +/// Currently, this only consists of chromatic aberration. +pub struct PostProcessingPlugin; + +/// Adds colored fringes to the edges of objects in the scene. +/// +/// [Chromatic aberration] simulates the effect when lenses fail to focus all +/// colors of light toward a single point. It causes rainbow-colored streaks to +/// appear, which are especially apparent on the edges of objects. Chromatic +/// aberration is commonly used for collision effects, especially in horror +/// games. +/// +/// Bevy's implementation is based on that of *Inside* ([Gjøl & Svendsen 2016]). +/// It's based on a customizable lookup texture, which allows for changing the +/// color pattern. By default, the color pattern is simply a 3×1 pixel texture +/// consisting of red, green, and blue, in that order, but you can change it to +/// any image in order to achieve different effects. +/// +/// [Chromatic aberration]: https://en.wikipedia.org/wiki/Chromatic_aberration +/// +/// [Gjøl & Svendsen 2016]: https://github.com/playdeadgames/publications/blob/master/INSIDE/rendering_inside_gdc2016.pdf +#[derive(Reflect, Component, Clone)] +#[reflect(Component, Default)] +pub struct ChromaticAberration { + /// The lookup texture that determines the color gradient. + /// + /// By default, this is a 3×1 texel texture consisting of one red pixel, one + /// green pixel, and one blue texel, in that order. This recreates the most + /// typical chromatic aberration pattern. However, you can change it to + /// achieve different artistic effects. + /// + /// The texture is always sampled in its vertical center, so it should + /// ordinarily have a height of 1 texel. + pub color_lut: Handle, + + /// The size of the streaks around the edges of objects, as a fraction of + /// the window size. + /// + /// The default value is 0.2. + pub intensity: f32, + + /// A cap on the number of texture samples that will be performed. + /// + /// Higher values result in smoother-looking streaks but are slower. + /// + /// The default value is 8. + pub max_samples: u32, +} + +/// GPU pipeline data for the built-in postprocessing stack. +/// +/// This is stored in the render world. +#[derive(Resource)] +pub struct PostProcessingPipeline { + /// The layout of bind group 0, containing the source, LUT, and settings. + bind_group_layout: BindGroupLayout, + /// Specifies how to sample the source framebuffer texture. + source_sampler: Sampler, + /// Specifies how to sample the chromatic aberration gradient. + chromatic_aberration_lut_sampler: Sampler, +} + +/// A key that uniquely identifies a built-in postprocessing pipeline. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct PostProcessingPipelineKey { + /// The format of the source and destination textures. + texture_format: TextureFormat, +} + +/// A component attached to cameras in the render world that stores the +/// specialized pipeline ID for the built-in postprocessing stack. +#[derive(Component, Deref, DerefMut)] +pub struct PostProcessingPipelineId(CachedRenderPipelineId); + +/// The on-GPU version of the [`ChromaticAberration`] settings. +/// +/// See the documentation for [`ChromaticAberration`] for more information on +/// each of these fields. +#[derive(ShaderType)] +pub struct ChromaticAberrationUniform { + /// The intensity of the effect, in a fraction of the screen. + intensity: f32, + /// A cap on the number of samples of the source texture that the shader + /// will perform. + max_samples: u32, + /// Padding data. + unused_1: u32, + /// Padding data. + unused_2: u32, +} + +/// A resource, part of the render world, that stores the +/// [`ChromaticAberrationUniform`]s for each view. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct PostProcessingUniformBuffers { + chromatic_aberration: DynamicUniformBuffer, +} + +/// A component, part of the render world, that stores the appropriate byte +/// offset within the [`PostProcessingUniformBuffers`] for the camera it's +/// attached to. +#[derive(Component, Deref, DerefMut)] +pub struct PostProcessingUniformBufferOffsets { + chromatic_aberration: u32, +} + +/// The render node that runs the built-in postprocessing stack. +#[derive(Default)] +pub struct PostProcessingNode; + +impl Plugin for PostProcessingPlugin { + fn build(&self, app: &mut App) { + load_internal_asset!( + app, + POST_PROCESSING_SHADER_HANDLE, + "post_process.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + CHROMATIC_ABERRATION_SHADER_HANDLE, + "chromatic_aberration.wgsl", + Shader::from_wgsl + ); + + // Load the default chromatic aberration LUT. + let mut assets = app.world_mut().resource_mut::>(); + assets.insert( + DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE.id(), + Image::new( + Extent3d { + width: 3, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + DEFAULT_CHROMATIC_ABERRATION_LUT_DATA.to_vec(), + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::RENDER_WORLD, + ), + ); + + app.register_type::(); + app.add_plugins(ExtractComponentPlugin::::default()); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::>() + .init_resource::() + .add_systems( + Render, + ( + prepare_post_processing_pipelines, + prepare_post_processing_uniforms, + ) + .in_set(RenderSet::Prepare), + ) + .add_render_graph_node::>( + Core3d, + Node3d::PostProcessing, + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::DepthOfField, + Node3d::PostProcessing, + Node3d::Tonemapping, + ), + ) + .add_render_graph_node::>( + Core2d, + Node2d::PostProcessing, + ) + .add_render_graph_edges( + Core2d, + (Node2d::Bloom, Node2d::PostProcessing, Node2d::Tonemapping), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); + } +} + +impl Default for ChromaticAberration { + fn default() -> Self { + Self { + color_lut: DEFAULT_CHROMATIC_ABERRATION_LUT_HANDLE, + intensity: DEFAULT_CHROMATIC_ABERRATION_INTENSITY, + max_samples: DEFAULT_CHROMATIC_ABERRATION_MAX_SAMPLES, + } + } +} + +impl FromWorld for PostProcessingPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + // Create our single bind group layout. + let bind_group_layout = render_device.create_bind_group_layout( + Some("postprocessing bind group layout"), + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // Chromatic aberration source: + texture_2d(TextureSampleType::Float { filterable: true }), + // Chromatic aberration source sampler: + sampler(SamplerBindingType::Filtering), + // Chromatic aberration LUT: + texture_2d(TextureSampleType::Float { filterable: true }), + // Chromatic aberration LUT sampler: + sampler(SamplerBindingType::Filtering), + // Chromatic aberration settings: + uniform_buffer::(true), + ), + ), + ); + + // Both source and chromatic aberration LUTs should be sampled + // bilinearly. + + let source_sampler = render_device.create_sampler(&SamplerDescriptor { + mipmap_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + ..default() + }); + + let chromatic_aberration_lut_sampler = render_device.create_sampler(&SamplerDescriptor { + mipmap_filter: FilterMode::Linear, + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + ..default() + }); + + PostProcessingPipeline { + bind_group_layout, + source_sampler, + chromatic_aberration_lut_sampler, + } + } +} + +impl SpecializedRenderPipeline for PostProcessingPipeline { + type Key = PostProcessingPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + RenderPipelineDescriptor { + label: Some("postprocessing".into()), + layout: vec![self.bind_group_layout.clone()], + vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + fragment: Some(FragmentState { + shader: POST_PROCESSING_SHADER_HANDLE, + shader_defs: vec![], + entry_point: "fragment_main".into(), + targets: vec![Some(ColorTargetState { + format: key.texture_format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: default(), + depth_stencil: None, + multisample: default(), + push_constant_ranges: vec![], + } + } +} + +impl ViewNode for PostProcessingNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let post_processing_pipeline = world.resource::(); + let post_processing_uniform_buffers = world.resource::(); + let gpu_image_assets = world.resource::>(); + + // We need a render pipeline to be prepared. + let Some(pipeline) = pipeline_cache.get_render_pipeline(**pipeline_id) else { + return Ok(()); + }; + + // We need the chromatic aberration LUT to be present. + let Some(chromatic_aberration_lut) = gpu_image_assets.get(&chromatic_aberration.color_lut) + else { + return Ok(()); + }; + + // We need the postprocessing settings to be uploaded to the GPU. + let Some(chromatic_aberration_uniform_buffer_binding) = post_processing_uniform_buffers + .chromatic_aberration + .binding() + else { + return Ok(()); + }; + + // Use the [`PostProcessWrite`] infrastructure, since this is a + // full-screen pass. + let post_process = view_target.post_process_write(); + + let pass_descriptor = RenderPassDescriptor { + label: Some("postprocessing pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let bind_group = render_context.render_device().create_bind_group( + Some("postprocessing bind group"), + &post_processing_pipeline.bind_group_layout, + &BindGroupEntries::sequential(( + post_process.source, + &post_processing_pipeline.source_sampler, + &chromatic_aberration_lut.texture_view, + &post_processing_pipeline.chromatic_aberration_lut_sampler, + chromatic_aberration_uniform_buffer_binding, + )), + ); + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[**post_processing_uniform_buffer_offsets]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} + +/// Specializes the built-in postprocessing pipeline for each applicable view. +pub fn prepare_post_processing_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + post_processing_pipeline: Res, + views: Query<(Entity, &ExtractedView), With>, +) { + for (entity, view) in views.iter() { + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &post_processing_pipeline, + PostProcessingPipelineKey { + texture_format: if view.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + }, + ); + + commands + .entity(entity) + .insert(PostProcessingPipelineId(pipeline_id)); + } +} + +/// Gathers the built-in postprocessing settings for every view and uploads them +/// to the GPU. +pub fn prepare_post_processing_uniforms( + mut commands: Commands, + mut post_processing_uniform_buffers: ResMut, + render_device: Res, + render_queue: Res, + mut views: Query<(Entity, &ChromaticAberration)>, +) { + post_processing_uniform_buffers.clear(); + + // Gather up all the postprocessing settings. + for (view_entity, chromatic_aberration) in views.iter_mut() { + let chromatic_aberration_uniform_buffer_offset = + post_processing_uniform_buffers.push(&ChromaticAberrationUniform { + intensity: chromatic_aberration.intensity, + max_samples: chromatic_aberration.max_samples, + unused_1: 0, + unused_2: 0, + }); + commands + .entity(view_entity) + .insert(PostProcessingUniformBufferOffsets { + chromatic_aberration: chromatic_aberration_uniform_buffer_offset, + }); + } + + // Upload to the GPU. + post_processing_uniform_buffers.write_buffer(&render_device, &render_queue); +} + +impl ExtractComponent for ChromaticAberration { + type QueryData = Read; + + type QueryFilter = With; + + type Out = ChromaticAberration; + + fn extract_component( + chromatic_aberration: QueryItem<'_, Self::QueryData>, + ) -> Option { + // Skip the postprocessing phase entirely if the intensity is zero. + if chromatic_aberration.intensity > 0.0 { + Some(chromatic_aberration.clone()) + } else { + None + } + } +} diff --git a/crates/bevy_core_pipeline/src/post_process/post_process.wgsl b/crates/bevy_core_pipeline/src/post_process/post_process.wgsl new file mode 100644 index 00000000000000..6251ba66888100 --- /dev/null +++ b/crates/bevy_core_pipeline/src/post_process/post_process.wgsl @@ -0,0 +1,9 @@ +// Miscellaneous postprocessing effects, currently just chromatic aberration. + +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput +#import bevy_core_pipeline::post_processing::chromatic_aberration::chromatic_aberration + +@fragment +fn fragment_main(in: FullscreenVertexOutput) -> @location(0) vec4 { + return vec4(chromatic_aberration(in.uv), 1.0); +} diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 203581a2bf0d6b..d362c36f6c21fb 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -9,6 +9,7 @@ use bevy_render::{ renderer::RenderContext, view::{ViewDepthTexture, ViewUniformOffset}, }; +use bevy_utils::tracing::error; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -125,14 +126,23 @@ impl ViewNode for PrepassNode { { #[cfg(feature = "trace")] let _opaque_prepass_span = info_span!("opaque_prepass").entered(); - opaque_prepass_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = opaque_prepass_phase.render(&mut render_pass, world, view_entity) + { + error!("Error encountered while rendering the opaque prepass phase {err:?}"); + } } // Alpha masked draws if !alpha_mask_prepass_phase.is_empty() { #[cfg(feature = "trace")] let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered(); - alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity); + if let Err(err) = + alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity) + { + error!( + "Error encountered while rendering the alpha mask prepass phase {err:?}" + ); + } } // Skybox draw using a fullscreen triangle diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 6c0b059838a38b..59cfa908863c21 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -6,6 +6,7 @@ use bevy_ecs::{ schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, }; +use bevy_math::{Mat4, Quat}; use bevy_render::{ camera::Exposure, extract_component::{ @@ -22,6 +23,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniforms}, Render, RenderApp, RenderSet, }; +use bevy_transform::components::Transform; use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE}; use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms}; @@ -90,6 +92,21 @@ pub struct Skybox { /// 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, + + /// View space rotation applied to the skybox cubemap. + /// This is useful for users who require a different axis, such as the Z-axis, to serve + /// as the vertical axis. + pub rotation: Quat, +} + +impl Default for Skybox { + fn default() -> Self { + Skybox { + image: Handle::default(), + brightness: 0.0, + rotation: Quat::IDENTITY, + } + } } impl ExtractComponent for Skybox { @@ -106,6 +123,9 @@ impl ExtractComponent for Skybox { skybox.clone(), SkyboxUniforms { brightness: skybox.brightness * exposure, + transform: Transform::from_rotation(skybox.rotation) + .compute_matrix() + .inverse(), #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] _wasm_padding_8b: 0, #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] @@ -121,6 +141,7 @@ impl ExtractComponent for Skybox { #[derive(Component, ShaderType, Clone)] pub struct SkyboxUniforms { brightness: f32, + transform: Mat4, #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] _wasm_padding_8b: u32, #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] @@ -224,10 +245,9 @@ fn prepare_skybox_pipelines( pipeline_cache: Res, mut pipelines: ResMut>, pipeline: Res, - msaa: Res, - views: Query<(Entity, &ExtractedView), With>, + views: Query<(Entity, &ExtractedView, &Msaa), With>, ) { - for (entity, view) in &views { + for (entity, view, msaa) in &views { let pipeline_id = pipelines.specialize( &pipeline_cache, &pipeline, diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index 86d5d296f251de..f6f47d01167bc2 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -116,11 +116,10 @@ pub fn prepare_skybox_prepass_pipelines( mut commands: Commands, pipeline_cache: Res, mut pipelines: ResMut>, - msaa: Res, pipeline: Res, - views: Query<(Entity, Has), (With, With)>, + views: Query<(Entity, Has, &Msaa), (With, With)>, ) { - for (entity, normal_prepass) in &views { + for (entity, normal_prepass, msaa) in &views { let pipeline_key = SkyboxPrepassPipelineKey { samples: msaa.samples(), normal_prepass, diff --git a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl index 2a353706b5c9b9..41ac2140502952 100644 --- a/crates/bevy_core_pipeline/src/skybox/skybox.wgsl +++ b/crates/bevy_core_pipeline/src/skybox/skybox.wgsl @@ -3,6 +3,7 @@ struct SkyboxUniforms { brightness: f32, + transform: mat4x4, #ifdef SIXTEEN_BYTE_ALIGNMENT _wasm_padding_8b: u32, _wasm_padding_12b: u32, @@ -29,7 +30,12 @@ fn coords_to_ray_direction(position: vec2, viewport: vec4) -> vec3, e: vec2, subsample_indices: let d_xz = search_diag_2(tex_coord, vec2(-1.0, -1.0), &end); if (textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0, vec2(1, 0)).r > 0.0) { let d_yw = search_diag_2(tex_coord, vec2(1.0, 1.0), &end); - d = vec4(d.x, d_yw.x, d.z, d_yw.y); + d = vec4(d_xz.x, d_yw.x, d_xz.y, d_yw.y); d.y += f32(end.y > 0.9); } else { - d = vec4(d.x, 0.0, d.z, 0.0); + d = vec4(d_xz.x, 0.0, d_xz.y, 0.0); } if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 04d67f451d43ef..4a78af178e5037 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -34,10 +34,11 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget}, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; +use bevy_utils::tracing::warn; const TAA_SHADER_HANDLE: Handle = Handle::weak_from_u128(656865235226276); -/// Plugin for temporal anti-aliasing. Disables multisample anti-aliasing (MSAA). +/// Plugin for temporal anti-aliasing. /// /// See [`TemporalAntiAliasSettings`] for more details. pub struct TemporalAntiAliasPlugin; @@ -46,8 +47,7 @@ impl Plugin for TemporalAntiAliasPlugin { fn build(&self, app: &mut App) { load_internal_asset!(app, TAA_SHADER_HANDLE, "taa.wgsl", Shader::from_wgsl); - app.insert_resource(Msaa::Off) - .register_type::(); + app.register_type::(); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -118,6 +118,8 @@ pub struct TemporalAntiAliasBundle { /// /// # Usage Notes /// +/// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`]. +/// /// Requires that you add [`TemporalAntiAliasPlugin`] to your app, /// and add the [`DepthPrepass`], [`MotionVectorPrepass`], and [`TemporalJitter`] /// components to your camera. @@ -162,17 +164,23 @@ impl ViewNode for TemporalAntiAliasNode { &'static TemporalAntiAliasHistoryTextures, &'static ViewPrepassTextures, &'static TemporalAntiAliasPipelineId, + &'static Msaa, ); fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, view_target, taa_history_textures, prepass_textures, taa_pipeline_id): QueryItem< + (camera, view_target, taa_history_textures, prepass_textures, taa_pipeline_id, msaa): QueryItem< Self::ViewQuery, >, world: &World, ) -> Result<(), NodeRunError> { + if *msaa != Msaa::Off { + warn!("Temporal anti-aliasing requires MSAA to be disabled"); + return Ok(()); + } + let (Some(pipelines), Some(pipeline_cache)) = ( world.get_resource::(), world.get_resource::(), @@ -402,13 +410,13 @@ fn prepare_taa_history_textures( views: Query<(Entity, &ExtractedCamera, &ExtractedView), With>, ) { for (entity, camera, view) in &views { - if let Some(physical_viewport_size) = camera.physical_viewport_size { + if let Some(physical_target_size) = camera.physical_target_size { let mut texture_descriptor = TextureDescriptor { label: None, size: Extent3d { depth_or_array_layers: 1, - width: physical_viewport_size.x, - height: physical_viewport_size.y, + width: physical_target_size.x, + height: physical_target_size.y, }, mip_level_count: 1, sample_count: 1, diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 9a253363566548..1f99580dbe1b3b 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -17,7 +17,14 @@ impl Plugin for UpscalingPlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( Render, - prepare_view_upscaling_pipelines.in_set(RenderSet::Prepare), + // This system should probably technically be run *after* all of the other systems + // that might modify `PipelineCache` via interior mutability, but for now, + // we've chosen to simply ignore the ambiguities out of a desire for a better refactor + // and aversion to extensive and intrusive system ordering. + // See https://github.com/bevyengine/bevy/issues/14770 for more context. + prepare_view_upscaling_pipelines + .in_set(RenderSet::Prepare) + .ambiguous_with_all(), ); } } diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 1e1028d9d309a7..e3b1aa3109dab6 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -84,6 +84,14 @@ impl ViewNode for UpscalingNode { .command_encoder() .begin_render_pass(&pass_descriptor); + if let Some(camera) = camera { + if let Some(viewport) = &camera.viewport { + let size = viewport.physical_size; + let position = viewport.physical_position; + render_pass.set_scissor_rect(position.x, position.y, size.x, size.y); + } + } + render_pass.set_pipeline(pipeline); render_pass.set_bind_group(0, bind_group, &[]); render_pass.draw(0..3, 0..1); diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index 294cbdceda01c1..385ba359ba6d3a 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -21,5 +21,5 @@ syn = { version = "2.0", features = ["full"] } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_derive/compile_fail/tests/derive.rs b/crates/bevy_derive/compile_fail/tests/derive.rs index caabcbf332ea22..b918abe2733de5 100644 --- a/crates/bevy_derive/compile_fail/tests/derive.rs +++ b/crates/bevy_derive/compile_fail/tests/derive.rs @@ -1,4 +1,4 @@ fn main() -> compile_fail_utils::ui_test::Result<()> { - compile_fail_utils::test_multiple(["tests/deref_derive", "tests/deref_mut_derive"]) + compile_fail_utils::test_multiple("derive_deref", ["tests/deref_derive", "tests/deref_mut_derive"]) } diff --git a/crates/bevy_derive/src/app_plugin.rs b/crates/bevy_derive/src/app_plugin.rs deleted file mode 100644 index 108f567ac40a40..00000000000000 --- a/crates/bevy_derive/src/app_plugin.rs +++ /dev/null @@ -1,22 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -#[deprecated( - since = "0.14.0", - note = "The current dynamic plugin system is unsound and will be removed in 0.15." -)] -pub fn derive_dynamic_plugin(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let struct_name = &ast.ident; - - TokenStream::from(quote! { - #[no_mangle] - pub extern "C" fn _bevy_create_plugin() -> *mut dyn bevy::app::Plugin { - // make sure the constructor is the correct type. - let object = #struct_name {}; - let boxed = Box::new(object); - Box::into_raw(boxed) - } - }) -} diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs index 19fb1b71c7cc38..2151f4d09fe792 100644 --- a/crates/bevy_derive/src/lib.rs +++ b/crates/bevy_derive/src/lib.rs @@ -9,7 +9,6 @@ extern crate proc_macro; -mod app_plugin; mod bevy_main; mod derefs; mod enum_variant_meta; @@ -18,19 +17,6 @@ use bevy_macro_utils::{derive_label, BevyManifest}; use proc_macro::TokenStream; use quote::format_ident; -/// Generates a dynamic plugin entry point function for the given `Plugin` type. -/// -/// This is deprecated since 0.14. The current dynamic plugin system is unsound and will be removed in 0.15. -#[proc_macro_derive(DynamicPlugin)] -#[deprecated( - since = "0.14.0", - note = "The current dynamic plugin system is unsound and will be removed in 0.15." -)] -pub fn derive_dynamic_plugin(input: TokenStream) -> TokenStream { - #[allow(deprecated)] - app_plugin::derive_dynamic_plugin(input) -} - /// Implements [`Deref`] for structs. This is especially useful when utilizing the [newtype] pattern. /// /// For single-field structs, the implementation automatically uses that field. diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 5ef374ab827ca7..b06def4b3fd4e0 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -22,7 +22,9 @@ bevy_core = { path = "../bevy_core", version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_gizmos = { path = "../bevy_gizmos", version = "0.15.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", version = "0.15.0-dev", features = [ + "bevy_render", +] } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } @@ -46,5 +48,5 @@ ron = { version = "0.8.0", optional = true } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index 1ec0ebaac17b76..f6510f68a131d5 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -6,6 +6,7 @@ mod systems; pub use self::config::*; use bevy_app::prelude::*; +use bevy_ecs::schedule::IntoSystemConfigs; use bevy_time::TimeUpdateStrategy; use std::time::Duration; @@ -18,6 +19,7 @@ use std::time::Duration; /// This plugin is included within `DefaultPlugins` and `MinimalPlugins` when the `bevy_ci_testing` /// feature is enabled. It is recommended to only used this plugin during testing (manual or /// automatic), and disable it during regular development and for production builds. +#[derive(Default)] pub struct CiTestingPlugin; impl Plugin for CiTestingPlugin { @@ -45,9 +47,11 @@ impl Plugin for CiTestingPlugin { fixed_frame_time, ))); } - app.add_event::() .insert_resource(config) - .add_systems(Update, systems::send_events); + .add_systems( + Update, + systems::send_events.before(bevy_window::close_when_requested), + ); } } diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index 16547d7ce5fc6c..d5563a0bd946a8 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -47,6 +47,7 @@ pub mod states; /// /// Note: The third method is not recommended, as it requires you to remove the feature before /// creating a build for release to the public. +#[derive(Default)] pub struct DevToolsPlugin; impl Plugin for DevToolsPlugin { diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 8226774047a92a..972493e22d1b1d 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -24,20 +24,23 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } const-fnv1a-hash = "1.1.0" -# MacOS +# 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.30.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.31.0", optional = true, default-features = false, features = [ "apple-app-store", + "system", ] } -# 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.30.0", optional = true, default-features = false } +# Only include when on linux/windows/android/freebsd +[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android", target_os = "freebsd"))'.dependencies] +sysinfo = { version = "0.31.0", optional = true, default-features = false, features = [ + "system", +] } [lints] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index 3586960443414b..99a72706176cf2 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -13,7 +13,7 @@ use bevy_time::{Real, Time}; pub struct FrameTimeDiagnosticsPlugin; impl Plugin for FrameTimeDiagnosticsPlugin { - fn build(&self, app: &mut bevy_app::App) { + fn build(&self, app: &mut 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)) diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index bb0fb5d41d537e..bbb2de4fb10d37 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -37,7 +37,7 @@ impl Plugin for DiagnosticsPlugin { app.init_resource::(); #[cfg(feature = "sysinfo_plugin")] - app.init_resource::(); + app.init_resource::(); } } diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 981912ddc511cc..9037c6a8f82e9a 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -11,7 +11,7 @@ use bevy_ecs::system::Resource; /// * linux, /// * windows, /// * android, -/// * macos +/// * macOS /// /// NOT supported when using the `bevy/dynamic` feature even when using previously mentioned targets /// @@ -48,7 +48,7 @@ pub struct SystemInfo { pub memory: String, } -// NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on wasm +// NOTE: sysinfo fails to compile when using bevy dynamic or on iOS and does nothing on Wasm #[cfg(all( any( target_os = "linux", @@ -133,7 +133,7 @@ pub mod internal { sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage()); sys.refresh_memory(); - let current_cpu_usage = sys.global_cpu_info().cpu_usage().into(); + let current_cpu_usage = sys.global_cpu_usage().into(); // `memory()` fns return a value in bytes let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB; let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB; diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index d298aa298c8e03..33d2c697f76ef9 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -18,5 +18,5 @@ bevy_internal = { path = "../bevy_internal", version = "0.15.0-dev", default-fea workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_dynamic_plugin/Cargo.toml b/crates/bevy_dynamic_plugin/Cargo.toml deleted file mode 100644 index 090cf047548085..00000000000000 --- a/crates/bevy_dynamic_plugin/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "bevy_dynamic_plugin" -version = "0.15.0-dev" -edition = "2021" -description = "Provides dynamic plugin loading capabilities for non-wasm platforms" -homepage = "https://bevyengine.org" -repository = "https://github.com/bevyengine/bevy" -license = "MIT OR Apache-2.0" -keywords = ["bevy"] - -[dependencies] -# bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } - -# other -libloading = { version = "0.8" } -thiserror = "1.0" - -[lints] -workspace = true - -[package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] -all-features = true diff --git a/crates/bevy_dynamic_plugin/README.md b/crates/bevy_dynamic_plugin/README.md deleted file mode 100644 index 77b1b39af1d72c..00000000000000 --- a/crates/bevy_dynamic_plugin/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Bevy Dynamic Plugin - -[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) -[![Crates.io](https://img.shields.io/crates/v/bevy_dynamic_plugin.svg)](https://crates.io/crates/bevy_dynamic_plugin) -[![Downloads](https://img.shields.io/crates/d/bevy_dynamic_plugin.svg)](https://crates.io/crates/bevy_dynamic_plugin) -[![Docs](https://docs.rs/bevy_dynamic_plugin/badge.svg)](https://docs.rs/bevy_dynamic_plugin/latest/bevy_dynamic_plugin/) -[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/crates/bevy_dynamic_plugin/src/lib.rs b/crates/bevy_dynamic_plugin/src/lib.rs deleted file mode 100644 index 7483718ebc939b..00000000000000 --- a/crates/bevy_dynamic_plugin/src/lib.rs +++ /dev/null @@ -1,41 +0,0 @@ -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![doc( - html_logo_url = "https://bevyengine.org/assets/icon.png", - html_favicon_url = "https://bevyengine.org/assets/icon.png" -)] - -//! Bevy's dynamic plugin loading functionality. -//! -//! This crate allows loading dynamic libraries (`.dylib`, `.so`) that export a single -//! [`Plugin`](bevy_app::Plugin). For usage, see [`dynamically_load_plugin`]. -//! -//! # Deprecation -//! -//! The current dynamic plugin system is unsound and will be removed in 0.15. You may be interested -//! in the [Alternatives](#alternatives) listed below. If your use-case is not supported, please -//! consider commenting on [#13080](https://github.com/bevyengine/bevy/pull/13080) describing how -//! you use dynamic plugins in your project. -//! -//! # Warning -//! -//! Note that dynamic linking and loading is inherently unsafe because it allows executing foreign -//! code. Additionally, Rust does not have a stable ABI and may produce -//! incompatible libraries across Rust versions, or even subsequent compilations. This will not work -//! well in scenarios such as modding, but can work if the dynamic plugins and the main app are -//! built at the same time, such as with Downloadable Content (DLC) packs. -//! -//! # Alternatives -//! -//! You may be interested in these safer alternatives: -//! -//! - [Bevy Assets - Scripting]: Scripting and modding libraries for Bevy -//! - [Bevy Assets - Development tools]: Hot reloading and other development functionality -//! - [`stabby`]: Stable Rust ABI -//! -//! [Bevy Assets - Scripting]: https://bevyengine.org/assets/#scripting -//! [Bevy Assets - Development tools]: https://bevyengine.org/assets/#development-tools -//! [`stabby`]: https://github.com/ZettaScaleLabs/stabby - -mod loader; - -pub use loader::*; diff --git a/crates/bevy_dynamic_plugin/src/loader.rs b/crates/bevy_dynamic_plugin/src/loader.rs deleted file mode 100644 index 09c65d62730e22..00000000000000 --- a/crates/bevy_dynamic_plugin/src/loader.rs +++ /dev/null @@ -1,86 +0,0 @@ -#![allow(unsafe_code)] -#![allow(deprecated)] - -use libloading::{Library, Symbol}; -use std::ffi::OsStr; -use thiserror::Error; - -use bevy_app::{App, CreatePlugin, Plugin}; - -/// Errors that can occur when loading a dynamic plugin -#[derive(Debug, Error)] -#[deprecated( - since = "0.14.0", - note = "The current dynamic plugin system is unsound and will be removed in 0.15." -)] -pub enum DynamicPluginLoadError { - /// An error occurred when loading a dynamic library. - #[error("cannot load library for dynamic plugin: {0}")] - Library(#[source] libloading::Error), - /// An error occurred when loading a library without a valid Bevy plugin. - #[error("dynamic library does not contain a valid Bevy dynamic plugin")] - Plugin(#[source] libloading::Error), -} - -/// Dynamically links a plugin at the given path. The plugin must export a function with the -/// [`CreatePlugin`] signature named `_bevy_create_plugin`. -/// -/// # Safety -/// -/// The specified plugin must be linked against the exact same `libbevy.so` as this program. -/// In addition the `_bevy_create_plugin` symbol must not be manually created, but instead created -/// by deriving `DynamicPlugin` on a unit struct implementing [`Plugin`]. -/// -/// Dynamically loading plugins is orchestrated through dynamic linking. When linking against -/// foreign code, initialization routines may be run (as well as termination routines when the -/// program exits). The caller of this function is responsible for ensuring these routines are -/// sound. For more information, please see the safety section of [`libloading::Library::new`]. -#[deprecated( - since = "0.14.0", - note = "The current dynamic plugin system is unsound and will be removed in 0.15." -)] -pub unsafe fn dynamically_load_plugin>( - path: P, -) -> Result<(Library, Box), DynamicPluginLoadError> { - // SAFETY: Caller must follow the safety requirements of Library::new. - let lib = unsafe { Library::new(path).map_err(DynamicPluginLoadError::Library)? }; - - // SAFETY: Loaded plugins are not allowed to specify `_bevy_create_plugin` symbol manually, but - // must instead automatically generate it through `DynamicPlugin`. - let func: Symbol = unsafe { - lib.get(b"_bevy_create_plugin") - .map_err(DynamicPluginLoadError::Plugin)? - }; - - // SAFETY: `func` is automatically generated and is guaranteed to return a pointer created using - // `Box::into_raw`. - let plugin = unsafe { Box::from_raw(func()) }; - - Ok((lib, plugin)) -} - -/// An extension trait for [`App`] that allows loading dynamic plugins. -#[deprecated( - since = "0.14.0", - note = "The current dynamic plugin system is unsound and will be removed in 0.15." -)] -pub trait DynamicPluginExt { - /// Dynamically links a plugin at the given path, registering the plugin. - /// - /// For more details, see [`dynamically_load_plugin`]. - /// - /// # Safety - /// - /// See [`dynamically_load_plugin`]'s safety section. - unsafe fn load_plugin>(&mut self, path: P) -> &mut Self; -} - -impl DynamicPluginExt for App { - unsafe fn load_plugin>(&mut self, path: P) -> &mut Self { - // SAFETY: Follows the same safety requirements as `dynamically_load_plugin`. - let (lib, plugin) = unsafe { dynamically_load_plugin(path).unwrap() }; - std::mem::forget(lib); // Ensure that the library is not automatically unloaded - plugin.build(self); - self - } -} diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 54eee4ccd41b6b..173de89ad3c88b 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -11,11 +11,13 @@ categories = ["game-engines", "data-structures"] rust-version = "1.77.0" [features] +default = ["bevy_reflect"] trace = [] multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"] bevy_debug_stepping = [] -default = ["bevy_reflect"] serialize = ["dep:serde"] +track_change_detection = [] +reflect_functions = ["bevy_reflect", "bevy_reflect/functions"] [dependencies] bevy_ptr = { path = "../bevy_ptr", version = "0.15.0-dev" } @@ -53,5 +55,5 @@ path = "examples/change_detection.rs" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index 138b10d52ee26d..a85fc07900a7bb 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -109,8 +109,6 @@ fn print_time(time: Res() - .on_add(|mut world, _, _| { - world.resource_mut::().assert_order(0); - }) + .on_add(|mut world, _, _| world.resource_mut::().assert_order(0)) .on_insert(|mut world, _, _| world.resource_mut::().assert_order(1)) - .on_remove(|mut world, _, _| world.resource_mut::().assert_order(2)); + .on_replace(|mut world, _, _| world.resource_mut::().assert_order(2)) + .on_remove(|mut world, _, _| world.resource_mut::().assert_order(3)); let entity = world.spawn(A).id(); world.despawn(entity); - assert_eq!(3, world.resource::().0); + assert_eq!(4, world.resource::().0); } #[test] @@ -1192,7 +1313,7 @@ mod tests { let entity = world.spawn(AMacroHooks).id(); world.despawn(entity); - assert_eq!(3, world.resource::().0); + assert_eq!(4, world.resource::().0); } #[test] @@ -1201,21 +1322,37 @@ mod tests { world.init_resource::(); world .register_component_hooks::() - .on_add(|mut world, _, _| { - world.resource_mut::().assert_order(0); - }) - .on_insert(|mut world, _, _| { - world.resource_mut::().assert_order(1); - }) - .on_remove(|mut world, _, _| { - world.resource_mut::().assert_order(2); - }); + .on_add(|mut world, _, _| world.resource_mut::().assert_order(0)) + .on_insert(|mut world, _, _| world.resource_mut::().assert_order(1)) + .on_replace(|mut world, _, _| world.resource_mut::().assert_order(2)) + .on_remove(|mut world, _, _| world.resource_mut::().assert_order(3)); let mut entity = world.spawn_empty(); entity.insert(A); entity.remove::(); entity.flush(); - assert_eq!(3, world.resource::().0); + assert_eq!(4, world.resource::().0); + } + + #[test] + fn component_hook_order_replace() { + let mut world = World::new(); + world + .register_component_hooks::() + .on_replace(|mut world, _, _| world.resource_mut::().assert_order(0)) + .on_insert(|mut world, _, _| { + if let Some(mut r) = world.get_resource_mut::() { + r.assert_order(1); + } + }); + + let entity = world.spawn(A).id(); + world.init_resource::(); + let mut entity = world.entity_mut(entity); + entity.insert(A); + entity.insert_if_new(A); // this will not trigger on_replace or on_insert + entity.flush(); + assert_eq!(2, world.resource::().0); } #[test] @@ -1283,4 +1420,18 @@ mod tests { world.spawn(A).flush(); assert_eq!(4, world.resource::().0); } + + #[test] + fn insert_if_new() { + let mut world = World::new(); + let id = world.spawn(V("one")).id(); + let mut entity = world.entity_mut(id); + entity.insert_if_new(V("two")); + entity.insert_if_new((A, V("three"))); + entity.flush(); + // should still contain "one" + let entity = world.entity(id); + assert!(entity.contains::()); + assert_eq!(entity.get(), Some(&V("one"))); + } } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 03e8a40b55a023..b91bb9ea160a4b 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -5,9 +5,13 @@ use crate::{ ptr::PtrMut, system::Resource, }; +#[cfg(feature = "track_change_detection")] +use bevy_ptr::ThinSlicePtr; use bevy_ptr::{Ptr, UnsafeCellDeref}; use std::mem; use std::ops::{Deref, DerefMut}; +#[cfg(feature = "track_change_detection")] +use std::{cell::UnsafeCell, panic::Location}; /// The (arbitrarily chosen) minimum number of world tick increments between `check_tick` scans. /// @@ -63,6 +67,10 @@ pub trait DetectChanges { /// [`SystemChangeTick`](crate::system::SystemChangeTick) /// [`SystemParam`](crate::system::SystemParam). fn last_changed(&self) -> Tick; + + /// The location that last caused this to change. + #[cfg(feature = "track_change_detection")] + fn changed_by(&self) -> &'static Location<'static>; } /// Types that implement reliable change detection. @@ -167,6 +175,7 @@ pub trait DetectChangesMut: DetectChanges { /// # assert!(!score_changed.run((), &mut world)); /// ``` #[inline] + #[track_caller] fn set_if_neq(&mut self, value: Self::Inner) -> bool where Self::Inner: Sized + PartialEq, @@ -226,7 +235,7 @@ pub trait DetectChangesMut: DetectChanges { /// # score_changed.initialize(&mut world); /// # score_changed.run((), &mut world); /// # - /// # let mut score_changed_event = IntoSystem::into_system(on_event::()); + /// # let mut score_changed_event = IntoSystem::into_system(on_event::); /// # score_changed_event.initialize(&mut world); /// # score_changed_event.run((), &mut world); /// # @@ -280,6 +289,12 @@ macro_rules! change_detection_impl { fn last_changed(&self) -> Tick { *self.ticks.changed } + + #[inline] + #[cfg(feature = "track_change_detection")] + fn changed_by(&self) -> &'static Location<'static> { + self.changed_by + } } impl<$($generics),*: ?Sized $(+ $traits)?> Deref for $name<$($generics),*> { @@ -306,13 +321,23 @@ macro_rules! change_detection_mut_impl { type Inner = $target; #[inline] + #[track_caller] fn set_changed(&mut self) { *self.ticks.changed = self.ticks.this_run; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } } #[inline] + #[track_caller] fn set_last_changed(&mut self, last_changed: Tick) { *self.ticks.changed = last_changed; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } } #[inline] @@ -323,8 +348,13 @@ macro_rules! change_detection_mut_impl { impl<$($generics),* : ?Sized $(+ $traits)?> DerefMut for $name<$($generics),*> { #[inline] + #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { self.set_changed(); + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } self.value } } @@ -361,7 +391,9 @@ macro_rules! impl_methods { changed: self.ticks.changed, last_run: self.ticks.last_run, this_run: self.ticks.this_run, - } + }, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, } } @@ -391,9 +423,25 @@ macro_rules! impl_methods { Mut { value: f(self.value), ticks: self.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, } } + /// Optionally maps to an inner value by applying a function to the contained reference. + /// This is useful in a situation where you need to convert a `Mut` to a `Mut`, but only if `T` contains `U`. + /// + /// As with `map_unchanged`, you should never modify the argument passed to the closure. + pub fn filter_map_unchanged(self, f: impl FnOnce(&mut $target) -> Option<&mut U>) -> Option> { + let value = f(self.value); + value.map(|value| Mut { + value, + ticks: self.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, + }) + } + /// Allows you access to the dereferenced value of this pointer without immediately /// triggering change detection. pub fn as_deref_mut(&mut self) -> Mut<'_, <$target as Deref>::Target> @@ -501,6 +549,8 @@ impl<'w> From> for Ticks<'w> { pub struct Res<'w, T: ?Sized + Resource> { pub(crate) value: &'w T, pub(crate) ticks: Ticks<'w>, + #[cfg(feature = "track_change_detection")] + pub(crate) changed_by: &'static Location<'static>, } impl<'w, T: Resource> Res<'w, T> { @@ -513,6 +563,8 @@ impl<'w, T: Resource> Res<'w, T> { Self { value: this.value, ticks: this.ticks.clone(), + #[cfg(feature = "track_change_detection")] + changed_by: this.changed_by, } } @@ -529,6 +581,8 @@ impl<'w, T: Resource> From> for Res<'w, T> { Self { value: res.value, ticks: res.ticks.into(), + #[cfg(feature = "track_change_detection")] + changed_by: res.changed_by, } } } @@ -561,6 +615,8 @@ impl_debug!(Res<'w, T>, Resource); pub struct ResMut<'w, T: ?Sized + Resource> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, + #[cfg(feature = "track_change_detection")] + pub(crate) changed_by: &'w mut &'static Location<'static>, } impl<'w, 'a, T: Resource> IntoIterator for &'a ResMut<'w, T> @@ -600,6 +656,8 @@ impl<'w, T: Resource> From> for Mut<'w, T> { Mut { value: other.value, ticks: other.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: other.changed_by, } } } @@ -619,6 +677,8 @@ impl<'w, T: Resource> From> for Mut<'w, T> { pub struct NonSendMut<'w, T: ?Sized + 'static> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, + #[cfg(feature = "track_change_detection")] + pub(crate) changed_by: &'w mut &'static Location<'static>, } change_detection_impl!(NonSendMut<'w, T>, T,); @@ -633,6 +693,8 @@ impl<'w, T: 'static> From> for Mut<'w, T> { Mut { value: other.value, ticks: other.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: other.changed_by, } } } @@ -664,6 +726,8 @@ impl<'w, T: 'static> From> for Mut<'w, T> { pub struct Ref<'w, T: ?Sized> { pub(crate) value: &'w T, pub(crate) ticks: Ticks<'w>, + #[cfg(feature = "track_change_detection")] + pub(crate) changed_by: &'static Location<'static>, } impl<'w, T: ?Sized> Ref<'w, T> { @@ -680,6 +744,8 @@ impl<'w, T: ?Sized> Ref<'w, T> { Ref { value: f(self.value), ticks: self.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, } } @@ -700,6 +766,7 @@ impl<'w, T: ?Sized> Ref<'w, T> { changed: &'w Tick, last_run: Tick, this_run: Tick, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) -> Ref<'w, T> { Ref { value, @@ -709,6 +776,8 @@ impl<'w, T: ?Sized> Ref<'w, T> { last_run, this_run, }, + #[cfg(feature = "track_change_detection")] + changed_by: caller, } } } @@ -790,6 +859,8 @@ impl_debug!(Ref<'w, T>,); pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, + #[cfg(feature = "track_change_detection")] + pub(crate) changed_by: &'w mut &'static Location<'static>, } impl<'w, T: ?Sized> Mut<'w, T> { @@ -814,6 +885,7 @@ impl<'w, T: ?Sized> Mut<'w, T> { last_changed: &'w mut Tick, last_run: Tick, this_run: Tick, + #[cfg(feature = "track_change_detection")] caller: &'w mut &'static Location<'static>, ) -> Self { Self { value, @@ -823,6 +895,8 @@ impl<'w, T: ?Sized> Mut<'w, T> { last_run, this_run, }, + #[cfg(feature = "track_change_detection")] + changed_by: caller, } } } @@ -832,6 +906,8 @@ impl<'w, T: ?Sized> From> for Ref<'w, T> { Self { value: mut_ref.value, ticks: mut_ref.ticks.into(), + #[cfg(feature = "track_change_detection")] + changed_by: mut_ref.changed_by, } } } @@ -877,6 +953,8 @@ impl_debug!(Mut<'w, T>,); pub struct MutUntyped<'w> { pub(crate) value: PtrMut<'w>, pub(crate) ticks: TicksMut<'w>, + #[cfg(feature = "track_change_detection")] + pub(crate) changed_by: &'w mut &'static Location<'static>, } impl<'w> MutUntyped<'w> { @@ -901,6 +979,8 @@ impl<'w> MutUntyped<'w> { last_run: self.ticks.last_run, this_run: self.ticks.this_run, }, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, } } @@ -951,6 +1031,8 @@ impl<'w> MutUntyped<'w> { Mut { value: f(self.value), ticks: self.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, } } @@ -963,6 +1045,9 @@ impl<'w> MutUntyped<'w> { // SAFETY: `value` is `Aligned` and caller ensures the pointee type is `T`. value: unsafe { self.value.deref_mut() }, ticks: self.ticks, + // SAFETY: `caller` is `Aligned`. + #[cfg(feature = "track_change_detection")] + changed_by: self.changed_by, } } } @@ -986,22 +1071,39 @@ impl<'w> DetectChanges for MutUntyped<'w> { fn last_changed(&self) -> Tick { *self.ticks.changed } + + #[inline] + #[cfg(feature = "track_change_detection")] + fn changed_by(&self) -> &'static Location<'static> { + self.changed_by + } } impl<'w> DetectChangesMut for MutUntyped<'w> { type Inner = PtrMut<'w>; #[inline] + #[track_caller] fn set_changed(&mut self) { *self.ticks.changed = self.ticks.this_run; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } } #[inline] + #[track_caller] fn set_last_changed(&mut self, last_changed: Tick) { *self.ticks.changed = last_changed; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by = Location::caller(); + } } #[inline] + #[track_caller] fn bypass_change_detection(&mut self) -> &mut Self::Inner { &mut self.value } @@ -1020,16 +1122,71 @@ impl<'w, T> From> for MutUntyped<'w> { MutUntyped { value: value.value.into(), ticks: value.ticks, + #[cfg(feature = "track_change_detection")] + changed_by: value.changed_by, } } } +/// A type alias to [`&'static Location<'static>`](std::panic::Location) when the `track_change_detection` feature is +/// enabled, and the unit type `()` when it is not. +/// +/// This is primarily used in places where `#[cfg(...)]` attributes are not allowed, such as +/// function return types. Because unit is a zero-sized type, it is the equivalent of not using a +/// `Location` at all. +/// +/// Please use this type sparingly: prefer normal `#[cfg(...)]` attributes when possible. +#[cfg(feature = "track_change_detection")] +pub(crate) type MaybeLocation = &'static Location<'static>; + +/// A type alias to [`&'static Location<'static>`](std::panic::Location) when the `track_change_detection` feature is +/// enabled, and the unit type `()` when it is not. +/// +/// This is primarily used in places where `#[cfg(...)]` attributes are not allowed, such as +/// function return types. Because unit is a zero-sized type, it is the equivalent of not using a +/// `Location` at all. +/// +/// Please use this type sparingly: prefer normal `#[cfg(...)]` attributes when possible. +#[cfg(not(feature = "track_change_detection"))] +pub(crate) type MaybeLocation = (); + +/// A type alias to `&UnsafeCell<&'static Location<'static>>` when the `track_change_detection` +/// feature is enabled, and the unit type `()` when it is not. +/// +/// See [`MaybeLocation`] for further information. +#[cfg(feature = "track_change_detection")] +pub(crate) type MaybeUnsafeCellLocation<'a> = &'a UnsafeCell<&'static Location<'static>>; + +/// A type alias to `&UnsafeCell<&'static Location<'static>>` when the `track_change_detection` +/// feature is enabled, and the unit type `()` when it is not. +/// +/// See [`MaybeLocation`] for further information. +#[cfg(not(feature = "track_change_detection"))] +pub(crate) type MaybeUnsafeCellLocation<'a> = (); + +/// A type alias to `ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>` when the +/// `track_change_detection` feature is enabled, and the unit type `()` when it is not. +/// +/// See [`MaybeLocation`] for further information. +#[cfg(feature = "track_change_detection")] +pub(crate) type MaybeThinSlicePtrLocation<'w> = + ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>; + +/// A type alias to `ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>` when the +/// `track_change_detection` feature is enabled, and the unit type `()` when it is not. +/// +/// See [`MaybeLocation`] for further information. +#[cfg(not(feature = "track_change_detection"))] +pub(crate) type MaybeThinSlicePtrLocation<'w> = (); + #[cfg(test)] mod tests { use bevy_ecs_macros::Resource; use bevy_ptr::PtrMut; use bevy_reflect::{FromType, ReflectFromPtr}; use std::ops::{Deref, DerefMut}; + #[cfg(feature = "track_change_detection")] + use std::panic::Location; use crate::{ self as bevy_ecs, @@ -1160,9 +1317,14 @@ mod tests { this_run: Tick::new(4), }; let mut res = R {}; + #[cfg(feature = "track_change_detection")] + let mut caller = Location::caller(); + let res_mut = ResMut { value: &mut res, ticks, + #[cfg(feature = "track_change_detection")] + changed_by: &mut caller, }; let into_mut: Mut = res_mut.into(); @@ -1179,6 +1341,8 @@ mod tests { changed: Tick::new(3), }; let mut res = R {}; + #[cfg(feature = "track_change_detection")] + let mut caller = Location::caller(); let val = Mut::new( &mut res, @@ -1186,6 +1350,8 @@ mod tests { &mut component_ticks.changed, Tick::new(2), // last_run Tick::new(4), // this_run + #[cfg(feature = "track_change_detection")] + &mut caller, ); assert!(!val.is_added()); @@ -1205,9 +1371,14 @@ mod tests { this_run: Tick::new(4), }; let mut res = R {}; + #[cfg(feature = "track_change_detection")] + let mut caller = Location::caller(); + let non_send_mut = NonSendMut { value: &mut res, ticks, + #[cfg(feature = "track_change_detection")] + changed_by: &mut caller, }; let into_mut: Mut = non_send_mut.into(); @@ -1236,9 +1407,14 @@ mod tests { }; let mut outer = Outer(0); + #[cfg(feature = "track_change_detection")] + let mut caller = Location::caller(); + let ptr = Mut { value: &mut outer, ticks, + #[cfg(feature = "track_change_detection")] + changed_by: &mut caller, }; assert!(!ptr.is_changed()); @@ -1321,9 +1497,14 @@ mod tests { }; let mut value: i32 = 5; + #[cfg(feature = "track_change_detection")] + let mut caller = Location::caller(); + let value = MutUntyped { value: PtrMut::from(&mut value), ticks, + #[cfg(feature = "track_change_detection")] + changed_by: &mut caller, }; let reflect_from_ptr = >::from_type(); @@ -1354,9 +1535,14 @@ mod tests { this_run: Tick::new(4), }; let mut c = C {}; + #[cfg(feature = "track_change_detection")] + let mut caller = Location::caller(); + let mut_typed = Mut { value: &mut c, ticks, + #[cfg(feature = "track_change_detection")] + changed_by: &mut caller, }; let into_mut: MutUntyped = mut_typed.into(); diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 28a20315c93222..161db19b9a0e97 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -100,6 +100,7 @@ use std::{ /// Alternatively to the example shown in [`ComponentHooks`]' documentation, hooks can be configured using following attributes: /// - `#[component(on_add = on_add_function)]` /// - `#[component(on_insert = on_insert_function)]` +/// - `#[component(on_replace = on_replace_function)]` /// - `#[component(on_remove = on_remove_function)]` /// /// ``` @@ -114,8 +115,8 @@ use std::{ /// // Another possible way of configuring hooks: /// // #[component(on_add = my_on_add_hook, on_insert = my_on_insert_hook)] /// // -/// // We don't have a remove hook, so we can leave it out: -/// // #[component(on_remove = my_on_remove_hook)] +/// // We don't have a replace or remove hook, so we can leave them out: +/// // #[component(on_replace = my_on_replace_hook, on_remove = my_on_remove_hook)] /// struct ComponentA; /// /// fn my_on_add_hook(world: DeferredWorld, entity: Entity, id: ComponentId) { @@ -280,6 +281,7 @@ pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, Entity, ComponentId); pub struct ComponentHooks { pub(crate) on_add: Option, pub(crate) on_insert: Option, + pub(crate) on_replace: Option, pub(crate) on_remove: Option, } @@ -314,6 +316,28 @@ impl ComponentHooks { .expect("Component id: {:?}, already has an on_insert hook") } + /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, + /// such as being replaced (with `.insert`) or removed. + /// + /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, + /// allowing access to the previous data just before it is dropped. + /// This hook does *not* run if the entity did not already have this component. + /// + /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_replace` hook + pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_replace(hook) + .expect("Component id: {:?}, already has an on_replace hook") + } + /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. /// Despawning an entity counts as removing all of its components. /// @@ -351,6 +375,19 @@ impl ComponentHooks { Some(self) } + /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed + /// + /// This is a fallible version of [`Self::on_replace`]. + /// + /// Returns `None` if the component already has an `on_replace` hook. + pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_replace.is_some() { + return None; + } + self.on_replace = Some(hook); + Some(self) + } + /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. /// /// This is a fallible version of [`Self::on_remove`]. @@ -442,6 +479,9 @@ impl ComponentInfo { if self.hooks().on_insert.is_some() { flags.insert(ArchetypeFlags::ON_INSERT_HOOK); } + if self.hooks().on_replace.is_some() { + flags.insert(ArchetypeFlags::ON_REPLACE_HOOK); + } if self.hooks().on_remove.is_some() { flags.insert(ArchetypeFlags::ON_REMOVE_HOOK); } diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 656687cdcd7a72..cca781d06b652c 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -142,7 +142,7 @@ type IdCursor = isize; /// [`Query::get`]: crate::system::Query::get /// [`World`]: crate::world::World /// [SemVer]: https://semver.org/ -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect_value(Hash, PartialEq))] #[cfg_attr( @@ -385,11 +385,47 @@ impl<'de> Deserialize<'de> for Entity { D: serde::Deserializer<'de>, { use serde::de::Error; - let id: u64 = serde::de::Deserialize::deserialize(deserializer)?; + let id: u64 = Deserialize::deserialize(deserializer)?; Entity::try_from_bits(id).map_err(D::Error::custom) } } +/// Outputs the full entity identifier, including the index, generation, and the raw bits. +/// +/// This takes the format: `{index}v{generation}#{bits}`. +/// +/// # Usage +/// +/// Prefer to use this format for debugging and logging purposes. Because the output contains +/// the raw bits, it is easy to check it against serialized scene data. +/// +/// Example serialized scene data: +/// ```text +/// ( +/// ... +/// entities: { +/// 4294967297: ( <--- Raw Bits +/// components: { +/// ... +/// ), +/// ... +/// ) +/// ``` +impl fmt::Debug for Entity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}v{}#{}", + self.index(), + self.generation(), + self.to_bits() + ) + } +} + +/// Outputs the short entity identifier, including the index and generation. +/// +/// This takes the format: `{index}v{generation}`. impl fmt::Display for Entity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}v{}", self.index(), self.generation()) @@ -968,13 +1004,11 @@ impl EntityLocation { #[cfg(test)] mod tests { use super::*; + use std::mem::size_of; #[test] fn entity_niche_optimization() { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::>() - ); + assert_eq!(size_of::(), size_of::>()); } #[test] @@ -1152,6 +1186,15 @@ mod tests { } } + #[test] + fn entity_debug() { + let entity = Entity::from_raw(42); + let string = format!("{:?}", entity); + assert!(string.contains("42")); + assert!(string.contains("v1")); + assert!(string.contains(format!("#{}", entity.to_bits()).as_str())); + } + #[test] fn entity_display() { let entity = Entity::from_raw(42); diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index f31f9b7c0cb304..ab56ac58ae5b49 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -1,4 +1,4 @@ -use crate::component::Component; +use crate::{component::Component, traversal::Traversal}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use std::{ @@ -34,7 +34,19 @@ use std::{ label = "invalid `Event`", note = "consider annotating `{Self}` with `#[derive(Event)]`" )] -pub trait Event: Component {} +pub trait Event: Component { + /// The component that describes which Entity to propagate this event to next, when [propagation] is enabled. + /// + /// [propagation]: crate::observer::Trigger::propagate + type Traversal: Traversal; + + /// When true, this event will always attempt to propagate when [triggered], without requiring a call + /// to [`Trigger::propagate`]. + /// + /// [triggered]: crate::system::Commands::trigger_targets + /// [`Trigger::propagate`]: crate::observer::Trigger::propagate + const AUTO_PROPAGATE: bool = false; +} /// An `EventId` uniquely identifies an event stored in a specific [`World`]. /// diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index da573baf04bd7d..a08a62053e1078 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1,7 +1,9 @@ // FIXME(11590): remove this once the lint is fixed #![allow(unsafe_op_in_unsafe_fn)] #![doc = include_str!("../README.md")] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +#![allow(internal_features)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![allow(unsafe_code)] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", @@ -29,12 +31,16 @@ pub mod removal_detection; pub mod schedule; pub mod storage; pub mod system; +pub mod traversal; pub mod world; pub use bevy_ptr as ptr; /// Most commonly used re-exported types. pub mod prelude { + #[doc(hidden)] + #[cfg(feature = "reflect_functions")] + pub use crate::reflect::AppFunctionRegistry; #[doc(hidden)] #[cfg(feature = "bevy_reflect")] pub use crate::reflect::{ @@ -55,12 +61,13 @@ pub mod prelude { IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, system::{ - Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, - ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemBuilder, - SystemParamFunction, + Commands, Deferred, EntityCommand, EntityCommands, In, IntoSystem, Local, NonSend, + NonSendMut, ParallelCommands, ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, + System, SystemParamBuilder, SystemParamFunction, }, world::{ - EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, World, + Command, EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, + OnReplace, World, }, }; } @@ -69,6 +76,7 @@ pub mod prelude { mod tests { use crate as bevy_ecs; use crate::prelude::Or; + use crate::world::EntityMut; use crate::{ bundle::Bundle, change_detection::Ref, @@ -79,6 +87,7 @@ mod tests { world::{EntityRef, Mut, World}, }; use bevy_tasks::{ComputeTaskPool, TaskPool}; + use bevy_utils::HashSet; use std::num::NonZeroU32; use std::{ any::TypeId, @@ -89,9 +98,9 @@ mod tests { }, }; - #[derive(Component, Resource, Debug, PartialEq, Eq, Clone, Copy)] + #[derive(Component, Resource, Debug, PartialEq, Eq, Hash, Clone, Copy)] struct A(usize); - #[derive(Component, Debug, PartialEq, Eq, Clone, Copy)] + #[derive(Component, Debug, PartialEq, Eq, Hash, Clone, Copy)] struct B(usize); #[derive(Component, Debug, PartialEq, Eq, Clone, Copy)] struct C; @@ -124,7 +133,7 @@ mod tests { #[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] #[component(storage = "Table")] struct TableStored(&'static str); - #[derive(Component, Copy, Clone, PartialEq, Eq, Debug)] + #[derive(Component, Copy, Clone, PartialEq, Eq, Hash, Debug)] #[component(storage = "SparseSet")] struct SparseStored(u32); @@ -377,8 +386,9 @@ mod tests { .query::<(Entity, &A)>() .iter(&world) .map(|(e, &i)| (e, i)) - .collect::>(); - assert_eq!(ents, &[(e, A(123)), (f, A(456))]); + .collect::>(); + assert!(ents.contains(&(e, A(123)))); + assert!(ents.contains(&(f, A(456)))); } #[test] @@ -400,12 +410,15 @@ mod tests { let mut world = World::new(); let e = world.spawn((TableStored("abc"), A(123))).id(); let f = world.spawn((TableStored("def"), A(456), B(1))).id(); - let mut results = Vec::new(); + let mut results = HashSet::new(); world .query::<(Entity, &A)>() .iter(&world) - .for_each(|(e, &i)| results.push((e, i))); - assert_eq!(results, &[(e, A(123)), (f, A(456))]); + .for_each(|(e, &i)| { + results.insert((e, i)); + }); + assert!(results.contains(&(e, A(123)))); + assert!(results.contains(&(f, A(456)))); } #[test] @@ -552,8 +565,9 @@ mod tests { .query::<(Entity, Option<&B>, &A)>() .iter(&world) .map(|(e, b, &i)| (e, b.copied(), i)) - .collect::>(); - assert_eq!(ents, &[(e, None, A(123)), (f, Some(B(1)), A(456))]); + .collect::>(); + assert!(ents.contains(&(e, None, A(123)))); + assert!(ents.contains(&(f, Some(B(1)), A(456)))); } #[test] @@ -570,10 +584,10 @@ mod tests { .query::<(Entity, Option<&SparseStored>, &A)>() .iter(&world) .map(|(e, b, &i)| (e, b.copied(), i)) - .collect::>(); + .collect::>(); assert_eq!( ents, - &[(e, None, A(123)), (f, Some(SparseStored(1)), A(456))] + HashSet::from([(e, None, A(123)), (f, Some(SparseStored(1)), A(456))]) ); } @@ -604,8 +618,8 @@ mod tests { .query::<(Entity, &A, &B)>() .iter(&world) .map(|(e, &i, &b)| (e, i, b)) - .collect::>(), - &[(e1, A(1), B(3)), (e2, A(2), B(4))] + .collect::>(), + HashSet::from([(e1, A(1), B(3)), (e2, A(2), B(4))]) ); assert_eq!(world.entity_mut(e1).take::(), Some(A(1))); @@ -622,8 +636,8 @@ mod tests { .query::<(Entity, &B, &TableStored)>() .iter(&world) .map(|(e, &B(b), &TableStored(s))| (e, b, s)) - .collect::>(), - &[(e2, 4, "xyz"), (e1, 3, "abc")] + .collect::>(), + HashSet::from([(e2, 4, "xyz"), (e1, 3, "abc")]) ); world.entity_mut(e1).insert(A(43)); assert_eq!( @@ -631,8 +645,8 @@ mod tests { .query::<(Entity, &A, &B)>() .iter(&world) .map(|(e, &i, &b)| (e, i, b)) - .collect::>(), - &[(e2, A(2), B(4)), (e1, A(43), B(3))] + .collect::>(), + HashSet::from([(e2, A(2), B(4)), (e1, A(43), B(3))]) ); world.entity_mut(e1).insert(C); assert_eq!( @@ -921,25 +935,33 @@ mod tests { } } - fn get_filtered(world: &mut World) -> Vec { + fn get_filtered(world: &mut World) -> HashSet { world .query_filtered::() .iter(world) - .collect::>() + .collect::>() } - assert_eq!(get_filtered::>(&mut world), vec![e1, e3]); + assert_eq!( + get_filtered::>(&mut world), + HashSet::from([e1, e3]) + ); // ensure changing an entity's archetypes also moves its changed state world.entity_mut(e1).insert(C); - assert_eq!(get_filtered::>(&mut world), vec![e3, e1], "changed entities list should not change (although the order will due to archetype moves)"); + assert_eq!( + get_filtered::>(&mut world), + HashSet::from([e3, e1]), + "changed entities list should not change" + ); // spawning a new A entity should not change existing changed state world.entity_mut(e1).insert((A(0), B(0))); + assert_eq!( get_filtered::>(&mut world), - vec![e3, e1], + HashSet::from([e3, e1]), "changed entities list should not change" ); @@ -947,7 +969,7 @@ mod tests { assert!(world.despawn(e2)); assert_eq!( get_filtered::>(&mut world), - vec![e3, e1], + HashSet::from([e3, e1]), "changed entities list should not change" ); @@ -955,7 +977,7 @@ mod tests { assert!(world.despawn(e1)); assert_eq!( get_filtered::>(&mut world), - vec![e3], + HashSet::from([e3]), "e1 should no longer be returned" ); @@ -966,11 +988,11 @@ mod tests { let e4 = world.spawn_empty().id(); world.entity_mut(e4).insert(A(0)); - assert_eq!(get_filtered::>(&mut world), vec![e4]); - assert_eq!(get_filtered::>(&mut world), vec![e4]); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e4])); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e4])); world.entity_mut(e4).insert(A(1)); - assert_eq!(get_filtered::>(&mut world), vec![e4]); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e4])); world.clear_trackers(); @@ -979,9 +1001,9 @@ mod tests { world.entity_mut(e4).insert((A(0), B(0))); assert!(get_filtered::>(&mut world).is_empty()); - assert_eq!(get_filtered::>(&mut world), vec![e4]); - assert_eq!(get_filtered::>(&mut world), vec![e4]); - assert_eq!(get_filtered::>(&mut world), vec![e4]); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e4])); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e4])); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e4])); } #[test] @@ -1004,28 +1026,28 @@ mod tests { } } - fn get_filtered(world: &mut World) -> Vec { + fn get_filtered(world: &mut World) -> HashSet { world .query_filtered::() .iter(world) - .collect::>() + .collect::>() } assert_eq!( get_filtered::>(&mut world), - vec![e1, e3] + HashSet::from([e1, e3]) ); // ensure changing an entity's archetypes also moves its changed state world.entity_mut(e1).insert(C); - assert_eq!(get_filtered::>(&mut world), vec![e3, e1], "changed entities list should not change (although the order will due to archetype moves)"); + assert_eq!(get_filtered::>(&mut world), HashSet::from([e3, e1]), "changed entities list should not change (although the order will due to archetype moves)"); // spawning a new SparseStored entity should not change existing changed state world.entity_mut(e1).insert(SparseStored(0)); assert_eq!( get_filtered::>(&mut world), - vec![e3, e1], + HashSet::from([e3, e1]), "changed entities list should not change" ); @@ -1033,7 +1055,7 @@ mod tests { assert!(world.despawn(e2)); assert_eq!( get_filtered::>(&mut world), - vec![e3, e1], + HashSet::from([e3, e1]), "changed entities list should not change" ); @@ -1041,7 +1063,7 @@ mod tests { assert!(world.despawn(e1)); assert_eq!( get_filtered::>(&mut world), - vec![e3], + HashSet::from([e3]), "e1 should no longer be returned" ); @@ -1052,11 +1074,20 @@ mod tests { let e4 = world.spawn_empty().id(); world.entity_mut(e4).insert(SparseStored(0)); - assert_eq!(get_filtered::>(&mut world), vec![e4]); - assert_eq!(get_filtered::>(&mut world), vec![e4]); + assert_eq!( + get_filtered::>(&mut world), + HashSet::from([e4]) + ); + assert_eq!( + get_filtered::>(&mut world), + HashSet::from([e4]) + ); world.entity_mut(e4).insert(A(1)); - assert_eq!(get_filtered::>(&mut world), vec![e4]); + assert_eq!( + get_filtered::>(&mut world), + HashSet::from([e4]) + ); world.clear_trackers(); @@ -1065,7 +1096,10 @@ mod tests { world.entity_mut(e4).insert(SparseStored(0)); assert!(get_filtered::>(&mut world).is_empty()); - assert_eq!(get_filtered::>(&mut world), vec![e4]); + assert_eq!( + get_filtered::>(&mut world), + HashSet::from([e4]) + ); } #[test] @@ -1247,8 +1281,8 @@ mod tests { let results = query .iter(&world) .map(|(a, b)| (a.0, b.0)) - .collect::>(); - assert_eq!(results, vec![(1, "1"), (2, "2"), (3, "3"),]); + .collect::>(); + assert_eq!(results, HashSet::from([(1, "1"), (2, "2"), (3, "3"),])); let removed_bundle = world.entity_mut(e2).take::<(B, TableStored)>().unwrap(); assert_eq!(removed_bundle, (B(2), TableStored("2"))); @@ -1256,12 +1290,12 @@ mod tests { let results = query .iter(&world) .map(|(a, b)| (a.0, b.0)) - .collect::>(); - assert_eq!(results, vec![(1, "1"), (3, "3"),]); + .collect::>(); + assert_eq!(results, HashSet::from([(1, "1"), (3, "3"),])); let mut a_query = world.query::<&A>(); - let results = a_query.iter(&world).map(|a| a.0).collect::>(); - assert_eq!(results, vec![1, 3, 2]); + let results = a_query.iter(&world).map(|a| a.0).collect::>(); + assert_eq!(results, HashSet::from([1, 3, 2])); let entity_ref = world.entity(e2); assert_eq!( @@ -1358,6 +1392,26 @@ mod tests { world.query::<(&mut A, EntityRef)>(); } + #[test] + #[should_panic] + fn entity_ref_and_entity_mut_query_panic() { + let mut world = World::new(); + world.query::<(EntityRef, EntityMut)>(); + } + + #[test] + #[should_panic] + fn entity_mut_and_entity_mut_query_panic() { + let mut world = World::new(); + world.query::<(EntityMut, EntityMut)>(); + } + + #[test] + fn entity_ref_and_entity_ref_query_no_panic() { + let mut world = World::new(); + world.query::<(EntityRef, EntityRef)>(); + } + #[test] #[should_panic] fn mut_and_mut_query_panic() { @@ -1389,8 +1443,8 @@ mod tests { let mut expected = FilteredAccess::::default(); let a_id = world.components.get_id(TypeId::of::()).unwrap(); let b_id = world.components.get_id(TypeId::of::()).unwrap(); - expected.add_write(a_id); - expected.add_read(b_id); + expected.add_component_write(a_id); + expected.add_component_read(b_id); assert!( query.component_access.eq(&expected), "ComponentId access from query fetch and query filter should be combined" @@ -1739,7 +1793,8 @@ mod tests { ); } - // These fields are never read so we get a dead code lint here. + // These structs are primarily compilation tests to test the derive macros. Because they are + // never constructed, we have to manually silence the `dead_code` lint. #[allow(dead_code)] #[derive(Component)] struct ComponentA(u32); @@ -1748,12 +1803,15 @@ mod tests { #[derive(Component)] struct ComponentB(u32); + #[allow(dead_code)] #[derive(Bundle)] struct Simple(ComponentA); + #[allow(dead_code)] #[derive(Bundle)] struct Tuple(Simple, ComponentB); + #[allow(dead_code)] #[derive(Bundle)] struct Record { field0: Simple, diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index b3d689d449fbf2..600384d4784389 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -15,18 +15,21 @@ use bevy_utils::{EntityHashMap, HashMap}; use std::marker::PhantomData; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the -/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. +/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also +/// contains event propagation information. See [`Trigger::propagate`] for more information. pub struct Trigger<'w, E, B: Bundle = ()> { event: &'w mut E, + propagate: &'w mut bool, trigger: ObserverTrigger, _marker: PhantomData, } impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// Creates a new trigger for the given event and observer information. - pub fn new(event: &'w mut E, trigger: ObserverTrigger) -> Self { + pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { Self { event, + propagate, trigger, _marker: PhantomData, } @@ -56,6 +59,29 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { pub fn entity(&self) -> Entity { self.trigger.entity } + + /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. + /// + /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events + /// use `TraverseNone` which ends the path immediately and prevents propagation. + /// + /// To enable propagation, you must: + /// + Set [`Event::Traversal`] to the component you want to propagate along. + /// + Either call `propagate(true)` in the first observer or set [`Event::AUTO_PROPAGATE`] to `true`. + /// + /// You can prevent an event from propagating further using `propagate(false)`. + /// + /// [`Traversal`]: crate::traversal::Traversal + pub fn propagate(&mut self, should_propagate: bool) { + *self.propagate = should_propagate; + } + + /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. + /// + /// [`propagate`]: Trigger::propagate + pub fn get_propagate(&self) -> bool { + *self.propagate + } } /// A description of what an [`Observer`] observes. @@ -143,6 +169,7 @@ pub struct Observers { // Cached ECS observers to save a lookup most common triggers. on_add: CachedObservers, on_insert: CachedObservers, + on_replace: CachedObservers, on_remove: CachedObservers, // Map from trigger type to set of observers cache: HashMap, @@ -153,6 +180,7 @@ impl Observers { match event_type { ON_ADD => &mut self.on_add, ON_INSERT => &mut self.on_insert, + ON_REPLACE => &mut self.on_replace, ON_REMOVE => &mut self.on_remove, _ => self.cache.entry(event_type).or_default(), } @@ -162,6 +190,7 @@ impl Observers { match event_type { ON_ADD => Some(&self.on_add), ON_INSERT => Some(&self.on_insert), + ON_REPLACE => Some(&self.on_replace), ON_REMOVE => Some(&self.on_remove), _ => self.cache.get(&event_type), } @@ -174,6 +203,7 @@ impl Observers { entity: Entity, components: impl Iterator, data: &mut T, + propagate: &mut bool, ) { // SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld` let (mut world, observers) = unsafe { @@ -197,9 +227,9 @@ impl Observers { entity, }, data.into(), + propagate, ); }; - // Trigger observers listening for any kind of this trigger observers.map.iter().for_each(&mut trigger_observer); @@ -231,6 +261,7 @@ impl Observers { match event_type { ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), _ => None, } @@ -244,6 +275,7 @@ impl Observers { if self.on_add.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); } + if self .on_insert .component_observers @@ -251,6 +283,15 @@ impl Observers { { flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); } + + if self + .on_replace + .component_observers + .contains_key(&component_id) + { + flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); + } + if self .on_remove .component_observers @@ -262,7 +303,7 @@ impl Observers { } impl World { - /// Spawn a "global" [`Observer`] and returns it's [`Entity`]. + /// Spawns a "global" [`Observer`] and returns its [`Entity`]. pub fn observe( &mut self, system: impl IntoObserverSystem, @@ -272,12 +313,12 @@ impl World { /// Triggers the given `event`, which will run any observers watching for it. pub fn trigger(&mut self, event: impl Event) { - TriggerEvent { event, targets: () }.apply(self); + TriggerEvent { event, targets: () }.trigger(self); } /// Triggers the given `event` for the given `targets`, which will run any observers watching for it. pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) { - TriggerEvent { event, targets }.apply(self); + TriggerEvent { event, targets }.trigger(self); } /// Register an observer to the cache, called when an observer is created @@ -391,8 +432,11 @@ mod tests { use bevy_ptr::OwningPtr; use crate as bevy_ecs; - use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState}; + use crate::observer::{ + EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace, + }; use crate::prelude::*; + use crate::traversal::Traversal; #[derive(Component)] struct A; @@ -421,6 +465,24 @@ mod tests { } } + #[derive(Component)] + struct Parent(Entity); + + impl Traversal for Parent { + fn traverse(&self) -> Option { + Some(self.0) + } + } + + #[derive(Component)] + struct EventPropagating; + + impl Event for EventPropagating { + type Traversal = Parent; + + const AUTO_PROPAGATE: bool = true; + } + #[test] fn observer_order_spawn_despawn() { let mut world = World::new(); @@ -428,11 +490,12 @@ mod tests { world.observe(|_: Trigger, mut res: ResMut| res.assert_order(0)); world.observe(|_: Trigger, mut res: ResMut| res.assert_order(1)); - world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(3)); let entity = world.spawn(A).id(); world.despawn(entity); - assert_eq!(3, world.resource::().0); + assert_eq!(4, world.resource::().0); } #[test] @@ -442,13 +505,14 @@ mod tests { world.observe(|_: Trigger, mut res: ResMut| res.assert_order(0)); world.observe(|_: Trigger, mut res: ResMut| res.assert_order(1)); - world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(3)); let mut entity = world.spawn_empty(); entity.insert(A); entity.remove::(); entity.flush(); - assert_eq!(3, world.resource::().0); + assert_eq!(4, world.resource::().0); } #[test] @@ -458,13 +522,34 @@ mod tests { world.observe(|_: Trigger, mut res: ResMut| res.assert_order(0)); world.observe(|_: Trigger, mut res: ResMut| res.assert_order(1)); - world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(2)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(3)); let mut entity = world.spawn_empty(); entity.insert(S); entity.remove::(); entity.flush(); - assert_eq!(3, world.resource::().0); + assert_eq!(4, world.resource::().0); + } + + #[test] + fn observer_order_replace() { + let mut world = World::new(); + world.init_resource::(); + + let entity = world.spawn(A).id(); + + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(0)); + world.observe(|_: Trigger, mut res: ResMut| res.assert_order(1)); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + + let mut entity = world.entity_mut(entity); + entity.insert(A); + entity.flush(); + assert_eq!(2, world.resource::().0); } #[test] @@ -649,7 +734,7 @@ mod tests { world.spawn(ObserverState { // SAFETY: we registered `event_a` above and it matches the type of TriggerA descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) }, - runner: |mut world, _trigger, _ptr| { + runner: |mut world, _trigger, _ptr, _propagate| { world.resource_mut::().0 += 1; }, ..Default::default() @@ -662,4 +747,233 @@ mod tests { world.flush(); assert_eq!(1, world.resource::().0); } + + #[test] + fn observer_propagating() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, child); + world.flush(); + assert_eq!(2, world.resource::().0); + } + + #[test] + fn observer_propagating_redundant_dispatch_same_entity() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, [child, child]); + world.flush(); + assert_eq!(4, world.resource::().0); + } + + #[test] + fn observer_propagating_redundant_dispatch_parent_child() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, [child, parent]); + world.flush(); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn observer_propagating_halt() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child = world + .spawn(Parent(parent)) + .observe( + |mut trigger: Trigger, mut res: ResMut| { + res.0 += 1; + trigger.propagate(false); + }, + ) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, child); + world.flush(); + assert_eq!(1, world.resource::().0); + } + + #[test] + fn observer_propagating_join() { + let mut world = World::new(); + world.init_resource::(); + + let parent = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child_a = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| { + res.0 += 1; + }) + .id(); + + let child_b = world + .spawn(Parent(parent)) + .observe(|_: Trigger, mut res: ResMut| { + res.0 += 1; + }) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, [child_a, child_b]); + world.flush(); + assert_eq!(4, world.resource::().0); + } + + #[test] + fn observer_propagating_no_next() { + let mut world = World::new(); + world.init_resource::(); + + let entity = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, entity); + world.flush(); + assert_eq!(1, world.resource::().0); + } + + #[test] + fn observer_propagating_parallel_propagation() { + let mut world = World::new(); + world.init_resource::(); + + let parent_a = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child_a = world + .spawn(Parent(parent_a)) + .observe( + |mut trigger: Trigger, mut res: ResMut| { + res.0 += 1; + trigger.propagate(false); + }, + ) + .id(); + + let parent_b = world + .spawn_empty() + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + let child_b = world + .spawn(Parent(parent_b)) + .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, [child_a, child_b]); + world.flush(); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn observer_propagating_world() { + let mut world = World::new(); + world.init_resource::(); + + world.observe(|_: Trigger, mut res: ResMut| res.0 += 1); + + let grandparent = world.spawn_empty().id(); + let parent = world.spawn(Parent(grandparent)).id(); + let child = world.spawn(Parent(parent)).id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, child); + world.flush(); + assert_eq!(3, world.resource::().0); + } + + #[test] + fn observer_propagating_world_skipping() { + let mut world = World::new(); + world.init_resource::(); + + world.observe( + |trigger: Trigger, query: Query<&A>, mut res: ResMut| { + if query.get(trigger.entity()).is_ok() { + res.0 += 1; + } + }, + ); + + let grandparent = world.spawn(A).id(); + let parent = world.spawn(Parent(grandparent)).id(); + let child = world.spawn((A, Parent(parent))).id(); + + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger_targets(EventPropagating, child); + world.flush(); + assert_eq!(2, world.resource::().0); + } } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index f68441f270fcfd..1706ead914e0ea 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -20,7 +20,7 @@ pub struct ObserverState { impl Default for ObserverState { fn default() -> Self { Self { - runner: |_, _, _| {}, + runner: |_, _, _, _| {}, last_trigger_id: 0, despawned_watched_entities: 0, descriptor: Default::default(), @@ -86,7 +86,7 @@ impl Component for ObserverState { /// Type for function that is run when an observer is triggered. /// Typically refers to the default runner that runs the system stored in the associated [`Observer`] component, /// but can be overridden for custom behaviour. -pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut); +pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: &mut bool); /// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". /// @@ -257,7 +257,8 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut); /// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`]. /// /// When first added, [`Observer`] will also create an [`ObserverState`] component, which registers the observer with the [`World`] and -/// serves as the "source of truth" of the observer. +/// serves as the "source of truth" of the observer. [`ObserverState`] can be used to filter for [`Observer`] [`Entity`]s, e.g. +/// [`With`]. /// /// [`SystemParam`]: crate::system::SystemParam pub struct Observer { @@ -358,6 +359,7 @@ fn observer_system_runner( mut world: DeferredWorld, observer_trigger: ObserverTrigger, ptr: PtrMut, + propagate: &mut bool, ) { let world = world.as_unsafe_world_cell(); // SAFETY: Observer was triggered so must still exist in world @@ -374,15 +376,18 @@ fn observer_system_runner( }; // TODO: Move this check into the observer cache to avoid dynamic dispatch - // SAFETY: We only access world metadata - let last_trigger = unsafe { world.world_metadata() }.last_trigger_id(); + let last_trigger = world.last_trigger_id(); if state.last_trigger_id == last_trigger { return; } state.last_trigger_id = last_trigger; - // SAFETY: Caller ensures `ptr` is castable to `&mut T` - let trigger: Trigger = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger); + let trigger: Trigger = Trigger::new( + // SAFETY: Caller ensures `ptr` is castable to `&mut T` + unsafe { ptr.deref_mut() }, + propagate, + observer_trigger, + ); // SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out. // Additionally, IntoObserverSystem is only implemented for functions starting // with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually, diff --git a/crates/bevy_ecs/src/observer/trigger_event.rs b/crates/bevy_ecs/src/observer/trigger_event.rs index ff4000c8ee6889..6323eb42c9db8a 100644 --- a/crates/bevy_ecs/src/observer/trigger_event.rs +++ b/crates/bevy_ecs/src/observer/trigger_event.rs @@ -14,13 +14,21 @@ pub struct TriggerEvent { pub targets: Targets, } -impl Command for TriggerEvent { - fn apply(mut self, world: &mut World) { +impl TriggerEvent { + pub(super) fn trigger(mut self, world: &mut World) { let event_type = world.init_component::(); trigger_event(world, event_type, &mut self.event, self.targets); } } +impl Command + for TriggerEvent +{ + fn apply(self, world: &mut World) { + self.trigger(world); + } +} + /// Emit a trigger for a dynamic component id. This is unsafe and must be verified manually. pub struct EmitDynamicTrigger { event_type: ComponentId, @@ -41,39 +49,43 @@ impl EmitDynamicTrigger { } } -impl Command for EmitDynamicTrigger { +impl Command + for EmitDynamicTrigger +{ fn apply(mut self, world: &mut World) { trigger_event(world, self.event_type, &mut self.event_data, self.targets); } } #[inline] -fn trigger_event( +fn trigger_event( world: &mut World, event_type: ComponentId, event_data: &mut E, targets: Targets, ) { let mut world = DeferredWorld::from(world); - if targets.entities().len() == 0 { + if targets.entities().is_empty() { // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` unsafe { - world.trigger_observers_with_data( + world.trigger_observers_with_data::<_, E::Traversal>( event_type, Entity::PLACEHOLDER, targets.components(), event_data, + false, ); }; } else { for target in targets.entities() { // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` unsafe { - world.trigger_observers_with_data( + world.trigger_observers_with_data::<_, E::Traversal>( event_type, - target, + *target, targets.components(), event_data, + E::AUTO_PROPAGATE, ); }; } @@ -86,80 +98,90 @@ fn trigger_event( /// /// [`Trigger`]: crate::observer::Trigger /// [`Observer`]: crate::observer::Observer -pub trait TriggerTargets: Send + Sync + 'static { +pub trait TriggerTargets { /// The components the trigger should target. - fn components(&self) -> impl ExactSizeIterator; + fn components(&self) -> &[ComponentId]; /// The entities the trigger should target. - fn entities(&self) -> impl ExactSizeIterator; + fn entities(&self) -> &[Entity]; } impl TriggerTargets for () { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } impl TriggerTargets for Entity { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - std::iter::once(*self) + fn entities(&self) -> &[Entity] { + std::slice::from_ref(self) } } impl TriggerTargets for Vec { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn entities(&self) -> &[Entity] { + self.as_slice() } } impl TriggerTargets for [Entity; N] { - fn components(&self) -> impl ExactSizeIterator { - [].into_iter() + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn entities(&self) -> &[Entity] { + self.as_slice() } } impl TriggerTargets for ComponentId { - fn components(&self) -> impl ExactSizeIterator { - std::iter::once(*self) + fn components(&self) -> &[ComponentId] { + std::slice::from_ref(self) } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } impl TriggerTargets for Vec { - fn components(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn components(&self) -> &[ComponentId] { + self.as_slice() } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + &[] } } impl TriggerTargets for [ComponentId; N] { - fn components(&self) -> impl ExactSizeIterator { - self.iter().copied() + fn components(&self) -> &[ComponentId] { + self.as_slice() + } + + fn entities(&self) -> &[Entity] { + &[] + } +} + +impl TriggerTargets for &Vec { + fn components(&self) -> &[ComponentId] { + &[] } - fn entities(&self) -> impl ExactSizeIterator { - [].into_iter() + fn entities(&self) -> &[Entity] { + self.as_slice() } } diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 27a8c9532e660e..6769a754ca030c 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1,7 +1,7 @@ use crate::storage::SparseSetIndex; -use bevy_utils::HashSet; use core::fmt; use fixedbitset::FixedBitSet; +use std::fmt::Debug; use std::marker::PhantomData; /// A wrapper struct to make Debug representations of [`FixedBitSet`] easier @@ -35,7 +35,7 @@ impl<'a, T: SparseSetIndex> FormattedBitSet<'a, T> { } } -impl<'a, T: SparseSetIndex + fmt::Debug> fmt::Debug for FormattedBitSet<'a, T> { +impl<'a, T: SparseSetIndex + Debug> Debug for FormattedBitSet<'a, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_list() .entries(self.bit_set.ones().map(T::get_sparse_set_index)) @@ -47,33 +47,88 @@ impl<'a, T: SparseSetIndex + fmt::Debug> fmt::Debug for FormattedBitSet<'a, T> { /// /// Used internally to ensure soundness during system initialization and execution. /// See the [`is_compatible`](Access::is_compatible) and [`get_conflicts`](Access::get_conflicts) functions. -#[derive(Clone, Eq, PartialEq)] +#[derive(Eq, PartialEq)] pub struct Access { - /// All accessed elements. - reads_and_writes: FixedBitSet, - /// The exclusively-accessed elements. - writes: FixedBitSet, - /// Is `true` if this has access to all elements in the collection. + /// All accessed components. + component_read_and_writes: FixedBitSet, + /// The exclusively-accessed components. + component_writes: FixedBitSet, + /// All accessed resources. + resource_read_and_writes: FixedBitSet, + /// The exclusively-accessed resources. + resource_writes: FixedBitSet, + /// Is `true` if this has access to all components. + /// (Note that this does not include `Resources`) + reads_all_components: bool, + /// Is `true` if this has mutable access to all components. + /// (Note that this does not include `Resources`) + writes_all_components: bool, + /// Is `true` if this has access to all resources. /// This field is a performance optimization for `&World` (also harder to mess up for soundness). - reads_all: bool, - /// Is `true` if this has mutable access to all elements in the collection. + reads_all_resources: bool, + /// Is `true` if this has mutable access to all resources. /// If this is true, then `reads_all` must also be true. - writes_all: bool, - // Elements that are not accessed, but whose presence in an archetype affect query results. + writes_all_resources: bool, + // Components that are not accessed, but whose presence in an archetype affect query results. archetypal: FixedBitSet, marker: PhantomData, } -impl fmt::Debug for Access { +// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. +impl Clone for Access { + fn clone(&self) -> Self { + Self { + component_read_and_writes: self.component_read_and_writes.clone(), + component_writes: self.component_writes.clone(), + resource_read_and_writes: self.resource_read_and_writes.clone(), + resource_writes: self.resource_writes.clone(), + reads_all_components: self.reads_all_components, + writes_all_components: self.writes_all_components, + reads_all_resources: self.reads_all_resources, + writes_all_resources: self.writes_all_resources, + archetypal: self.archetypal.clone(), + marker: PhantomData, + } + } + + fn clone_from(&mut self, source: &Self) { + self.component_read_and_writes + .clone_from(&source.component_read_and_writes); + self.component_writes.clone_from(&source.component_writes); + self.resource_read_and_writes + .clone_from(&source.resource_read_and_writes); + self.resource_writes.clone_from(&source.resource_writes); + self.reads_all_components = source.reads_all_components; + self.writes_all_components = source.writes_all_components; + self.reads_all_resources = source.reads_all_resources; + self.writes_all_resources = source.writes_all_resources; + self.archetypal.clone_from(&source.archetypal); + } +} + +impl Debug for Access { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Access") .field( - "read_and_writes", - &FormattedBitSet::::new(&self.reads_and_writes), + "component_read_and_writes", + &FormattedBitSet::::new(&self.component_read_and_writes), ) - .field("writes", &FormattedBitSet::::new(&self.writes)) - .field("reads_all", &self.reads_all) - .field("writes_all", &self.writes_all) + .field( + "component_writes", + &FormattedBitSet::::new(&self.component_writes), + ) + .field( + "resource_read_and_writes", + &FormattedBitSet::::new(&self.resource_read_and_writes), + ) + .field( + "resource_writes", + &FormattedBitSet::::new(&self.resource_writes), + ) + .field("reads_all_components", &self.reads_all_components) + .field("writes_all_components", &self.writes_all_components) + .field("reads_all_resources", &self.reads_all_resources) + .field("writes_all_resources", &self.writes_all_resources) .field("archetypal", &FormattedBitSet::::new(&self.archetypal)) .finish() } @@ -89,31 +144,50 @@ impl Access { /// Creates an empty [`Access`] collection. pub const fn new() -> Self { Self { - reads_all: false, - writes_all: false, - reads_and_writes: FixedBitSet::new(), - writes: FixedBitSet::new(), + reads_all_resources: false, + writes_all_resources: false, + reads_all_components: false, + writes_all_components: false, + component_read_and_writes: FixedBitSet::new(), + component_writes: FixedBitSet::new(), + resource_read_and_writes: FixedBitSet::new(), + resource_writes: FixedBitSet::new(), archetypal: FixedBitSet::new(), marker: PhantomData, } } - /// Adds access to the element given by `index`. - pub fn add_read(&mut self, index: T) { - self.reads_and_writes + /// Adds access to the component given by `index`. + pub fn add_component_read(&mut self, index: T) { + self.component_read_and_writes + .grow_and_insert(index.sparse_set_index()); + } + + /// Adds exclusive access to the component given by `index`. + pub fn add_component_write(&mut self, index: T) { + self.component_read_and_writes + .grow_and_insert(index.sparse_set_index()); + self.component_writes + .grow_and_insert(index.sparse_set_index()); + } + + /// Adds access to the resource given by `index`. + pub fn add_resource_read(&mut self, index: T) { + self.resource_read_and_writes .grow_and_insert(index.sparse_set_index()); } - /// Adds exclusive access to the element given by `index`. - pub fn add_write(&mut self, index: T) { - self.reads_and_writes + /// Adds exclusive access to the resource given by `index`. + pub fn add_resource_write(&mut self, index: T) { + self.resource_read_and_writes + .grow_and_insert(index.sparse_set_index()); + self.resource_writes .grow_and_insert(index.sparse_set_index()); - self.writes.grow_and_insert(index.sparse_set_index()); } - /// Adds an archetypal (indirect) access to the element given by `index`. + /// Adds an archetypal (indirect) access to the component given by `index`. /// - /// This is for elements whose values are not accessed (and thus will never cause conflicts), + /// This is for components whose values are not accessed (and thus will never cause conflicts), /// but whose presence in an archetype may affect query results. /// /// Currently, this is only used for [`Has`]. @@ -123,29 +197,55 @@ impl Access { self.archetypal.grow_and_insert(index.sparse_set_index()); } - /// Returns `true` if this can access the element given by `index`. - pub fn has_read(&self, index: T) -> bool { - self.reads_all || self.reads_and_writes.contains(index.sparse_set_index()) + /// Returns `true` if this can access the component given by `index`. + pub fn has_component_read(&self, index: T) -> bool { + self.reads_all_components + || self + .component_read_and_writes + .contains(index.sparse_set_index()) + } + + /// Returns `true` if this can access any component. + pub fn has_any_component_read(&self) -> bool { + self.reads_all_components || !self.component_read_and_writes.is_clear() + } + + /// Returns `true` if this can exclusively access the component given by `index`. + pub fn has_component_write(&self, index: T) -> bool { + self.writes_all_components || self.component_writes.contains(index.sparse_set_index()) + } + + /// Returns `true` if this accesses any component mutably. + pub fn has_any_component_write(&self) -> bool { + self.writes_all_components || !self.component_writes.is_clear() + } + + /// Returns `true` if this can access the resource given by `index`. + pub fn has_resource_read(&self, index: T) -> bool { + self.reads_all_resources + || self + .resource_read_and_writes + .contains(index.sparse_set_index()) } - /// Returns `true` if this can access anything. - pub fn has_any_read(&self) -> bool { - self.reads_all || !self.reads_and_writes.is_clear() + /// Returns `true` if this can access any resource. + pub fn has_any_resource_read(&self) -> bool { + self.reads_all_resources || !self.resource_read_and_writes.is_clear() } - /// Returns `true` if this can exclusively access the element given by `index`. - pub fn has_write(&self, index: T) -> bool { - self.writes_all || self.writes.contains(index.sparse_set_index()) + /// Returns `true` if this can exclusively access the resource given by `index`. + pub fn has_resource_write(&self, index: T) -> bool { + self.writes_all_resources || self.resource_writes.contains(index.sparse_set_index()) } - /// Returns `true` if this accesses anything mutably. - pub fn has_any_write(&self) -> bool { - self.writes_all || !self.writes.is_clear() + /// Returns `true` if this accesses any resource mutably. + pub fn has_any_resource_write(&self) -> bool { + self.writes_all_resources || !self.resource_writes.is_clear() } - /// Returns true if this has an archetypal (indirect) access to the element given by `index`. + /// Returns true if this has an archetypal (indirect) access to the component given by `index`. /// - /// This is an element whose value is not accessed (and thus will never cause conflicts), + /// This is a component whose value is not accessed (and thus will never cause conflicts), /// but whose presence in an archetype may affect query results. /// /// Currently, this is only used for [`Has`]. @@ -155,47 +255,170 @@ impl Access { self.archetypal.contains(index.sparse_set_index()) } + /// Sets this as having access to all components (i.e. `EntityRef`). + #[inline] + pub fn read_all_components(&mut self) { + self.reads_all_components = true; + } + + /// Sets this as having mutable access to all components (i.e. `EntityMut`). + #[inline] + pub fn write_all_components(&mut self) { + self.reads_all_components = true; + self.writes_all_components = true; + } + + /// Sets this as having access to all resources (i.e. `&World`). + #[inline] + pub fn read_all_resources(&mut self) { + self.reads_all_resources = true; + } + + /// Sets this as having mutable access to all resources (i.e. `&mut World`). + #[inline] + pub fn write_all_resources(&mut self) { + self.reads_all_resources = true; + self.writes_all_resources = true; + } + /// Sets this as having access to all indexed elements (i.e. `&World`). + #[inline] pub fn read_all(&mut self) { - self.reads_all = true; + self.read_all_components(); + self.read_all_resources(); } - /// Sets this as having mutable access to all indexed elements (i.e. `EntityMut`). + /// Sets this as having mutable access to all indexed elements (i.e. `&mut World`). + #[inline] pub fn write_all(&mut self) { - self.reads_all = true; - self.writes_all = true; + self.write_all_components(); + self.write_all_resources(); + } + + /// Returns `true` if this has access to all components (i.e. `EntityRef`). + #[inline] + pub fn has_read_all_components(&self) -> bool { + self.reads_all_components + } + + /// Returns `true` if this has write access to all components (i.e. `EntityMut`). + #[inline] + pub fn has_write_all_components(&self) -> bool { + self.writes_all_components + } + + /// Returns `true` if this has access to all resources (i.e. `EntityRef`). + #[inline] + pub fn has_read_all_resources(&self) -> bool { + self.reads_all_resources + } + + /// Returns `true` if this has write access to all resources (i.e. `EntityMut`). + #[inline] + pub fn has_write_all_resources(&self) -> bool { + self.writes_all_resources } /// Returns `true` if this has access to all indexed elements (i.e. `&World`). pub fn has_read_all(&self) -> bool { - self.reads_all + self.has_read_all_components() && self.has_read_all_resources() } - /// Returns `true` if this has write access to all indexed elements (i.e. `EntityMut`). + /// Returns `true` if this has write access to all indexed elements (i.e. `&mut World`). pub fn has_write_all(&self) -> bool { - self.writes_all + self.has_write_all_components() && self.has_write_all_resources() } /// Removes all writes. pub fn clear_writes(&mut self) { - self.writes_all = false; - self.writes.clear(); + self.writes_all_resources = false; + self.writes_all_components = false; + self.component_writes.clear(); + self.resource_writes.clear(); } /// Removes all accesses. pub fn clear(&mut self) { - self.reads_all = false; - self.writes_all = false; - self.reads_and_writes.clear(); - self.writes.clear(); + self.reads_all_resources = false; + self.writes_all_resources = false; + self.reads_all_components = false; + self.writes_all_components = false; + self.component_read_and_writes.clear(); + self.component_writes.clear(); + self.resource_read_and_writes.clear(); + self.resource_writes.clear(); } /// Adds all access from `other`. pub fn extend(&mut self, other: &Access) { - self.reads_all = self.reads_all || other.reads_all; - self.writes_all = self.writes_all || other.writes_all; - self.reads_and_writes.union_with(&other.reads_and_writes); - self.writes.union_with(&other.writes); + self.reads_all_resources = self.reads_all_resources || other.reads_all_resources; + self.writes_all_resources = self.writes_all_resources || other.writes_all_resources; + self.reads_all_components = self.reads_all_components || other.reads_all_components; + self.writes_all_components = self.writes_all_components || other.writes_all_components; + self.component_read_and_writes + .union_with(&other.component_read_and_writes); + self.component_writes.union_with(&other.component_writes); + self.resource_read_and_writes + .union_with(&other.resource_read_and_writes); + self.resource_writes.union_with(&other.resource_writes); + } + + /// Returns `true` if the access and `other` can be active at the same time, + /// only looking at their component access. + /// + /// [`Access`] instances are incompatible if one can write + /// an element that the other can read or write. + pub fn is_components_compatible(&self, other: &Access) -> bool { + if self.writes_all_components { + return !other.has_any_component_read(); + } + + if other.writes_all_components { + return !self.has_any_component_read(); + } + + if self.reads_all_components { + return !other.has_any_component_write(); + } + + if other.reads_all_components { + return !self.has_any_component_write(); + } + + self.component_writes + .is_disjoint(&other.component_read_and_writes) + && other + .component_writes + .is_disjoint(&self.component_read_and_writes) + } + + /// Returns `true` if the access and `other` can be active at the same time, + /// only looking at their resource access. + /// + /// [`Access`] instances are incompatible if one can write + /// an element that the other can read or write. + pub fn is_resources_compatible(&self, other: &Access) -> bool { + if self.writes_all_resources { + return !other.has_any_resource_read(); + } + + if other.writes_all_resources { + return !self.has_any_resource_read(); + } + + if self.reads_all_resources { + return !other.has_any_resource_write(); + } + + if other.reads_all_resources { + return !self.has_any_resource_write(); + } + + self.resource_writes + .is_disjoint(&other.resource_read_and_writes) + && other + .resource_writes + .is_disjoint(&self.resource_read_and_writes) } /// Returns `true` if the access and `other` can be active at the same time. @@ -203,98 +426,149 @@ impl Access { /// [`Access`] instances are incompatible if one can write /// an element that the other can read or write. pub fn is_compatible(&self, other: &Access) -> bool { - if self.writes_all { - return !other.has_any_read(); + self.is_components_compatible(other) && self.is_resources_compatible(other) + } + + /// Returns `true` if the set's component access is a subset of another, i.e. `other`'s component access + /// contains at least all the values in `self`. + pub fn is_subset_components(&self, other: &Access) -> bool { + if self.writes_all_components { + return other.writes_all_components; } - if other.writes_all { - return !self.has_any_read(); + if other.writes_all_components { + return true; } - if self.reads_all { - return !other.has_any_write(); + if self.reads_all_components { + return other.reads_all_components; } - if other.reads_all { - return !self.has_any_write(); + if other.reads_all_components { + return self.component_writes.is_subset(&other.component_writes); } - self.writes.is_disjoint(&other.reads_and_writes) - && other.writes.is_disjoint(&self.reads_and_writes) + self.component_read_and_writes + .is_subset(&other.component_read_and_writes) + && self.component_writes.is_subset(&other.component_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; + /// Returns `true` if the set's resource access is a subset of another, i.e. `other`'s resource access + /// contains at least all the values in `self`. + pub fn is_subset_resources(&self, other: &Access) -> bool { + if self.writes_all_resources { + return other.writes_all_resources; } - if other.writes_all { + if other.writes_all_resources { return true; } - if self.reads_all { - return other.reads_all; + if self.reads_all_resources { + return other.reads_all_resources; } - if other.reads_all { - return self.writes.is_subset(&other.writes); + if other.reads_all_resources { + return self.resource_writes.is_subset(&other.resource_writes); } - self.reads_and_writes.is_subset(&other.reads_and_writes) - && self.writes.is_subset(&other.writes) + self.resource_read_and_writes + .is_subset(&other.resource_read_and_writes) + && self.resource_writes.is_subset(&other.resource_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 { + self.is_subset_components(other) && self.is_subset_resources(other) } /// 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(); - if self.reads_all { - // QUESTION: How to handle `other.writes_all`? - conflicts.extend(other.writes.ones()); + pub fn get_conflicts(&self, other: &Access) -> AccessConflicts { + let mut conflicts = FixedBitSet::new(); + if self.reads_all_components { + if other.writes_all_components { + return AccessConflicts::All; + } + conflicts.extend(other.component_writes.ones()); } - if other.reads_all { - // QUESTION: How to handle `self.writes_all`. - conflicts.extend(self.writes.ones()); + if other.reads_all_components { + if self.writes_all_components { + return AccessConflicts::All; + } + conflicts.extend(self.component_writes.ones()); } - if self.writes_all { - conflicts.extend(other.reads_and_writes.ones()); + if self.writes_all_components { + conflicts.extend(other.component_read_and_writes.ones()); } - if other.writes_all { - conflicts.extend(self.reads_and_writes.ones()); + if other.writes_all_components { + conflicts.extend(self.component_read_and_writes.ones()); + } + if self.reads_all_resources { + if other.writes_all_resources { + return AccessConflicts::All; + } + conflicts.extend(other.resource_writes.ones()); } - conflicts.extend(self.writes.intersection(&other.reads_and_writes)); - conflicts.extend(self.reads_and_writes.intersection(&other.writes)); - conflicts - .ones() - .map(SparseSetIndex::get_sparse_set_index) - .collect() + if other.reads_all_resources { + if self.writes_all_resources { + return AccessConflicts::All; + } + conflicts.extend(self.resource_writes.ones()); + } + if self.writes_all_resources { + conflicts.extend(other.resource_read_and_writes.ones()); + } + + if other.writes_all_resources { + conflicts.extend(self.resource_read_and_writes.ones()); + } + + conflicts.extend( + self.component_writes + .intersection(&other.component_read_and_writes), + ); + conflicts.extend( + self.component_read_and_writes + .intersection(&other.component_writes), + ); + conflicts.extend( + self.resource_writes + .intersection(&other.resource_read_and_writes), + ); + conflicts.extend( + self.resource_read_and_writes + .intersection(&other.resource_writes), + ); + AccessConflicts::Individual(conflicts) } - /// Returns the indices of the elements this has access to. - pub fn reads_and_writes(&self) -> impl Iterator + '_ { - self.reads_and_writes.ones().map(T::get_sparse_set_index) + /// Returns the indices of the components this has access to. + pub fn component_reads_and_writes(&self) -> impl Iterator + '_ { + self.component_read_and_writes + .ones() + .map(T::get_sparse_set_index) } - /// Returns the indices of the elements this has non-exclusive access to. - pub fn reads(&self) -> impl Iterator + '_ { - self.reads_and_writes - .difference(&self.writes) + /// Returns the indices of the components this has non-exclusive access to. + pub fn component_reads(&self) -> impl Iterator + '_ { + self.component_read_and_writes + .difference(&self.component_writes) .map(T::get_sparse_set_index) } - /// Returns the indices of the elements this has exclusive access to. - pub fn writes(&self) -> impl Iterator + '_ { - self.writes.ones().map(T::get_sparse_set_index) + /// Returns the indices of the components this has exclusive access to. + pub fn component_writes(&self) -> impl Iterator + '_ { + self.component_writes.ones().map(T::get_sparse_set_index) } - /// Returns the indices of the elements that this has an archetypal access to. + /// Returns the indices of the components that this has an archetypal access to. /// - /// These are elements whose values are not accessed (and thus will never cause conflicts), + /// These are components whose values are not accessed (and thus will never cause conflicts), /// but whose presence in an archetype may affect query results. /// /// Currently, this is only used for [`Has`]. @@ -325,7 +599,7 @@ impl Access { /// - `Query>` accesses nothing /// /// See comments the [`WorldQuery`](super::WorldQuery) impls of [`AnyOf`](super::AnyOf)/`Option`/[`Or`](super::Or) for more information. -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub struct FilteredAccess { pub(crate) access: Access, pub(crate) required: FixedBitSet, @@ -334,14 +608,27 @@ pub struct FilteredAccess { pub(crate) filter_sets: Vec>, } -impl Default for FilteredAccess { - fn default() -> Self { +// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. +impl Clone for FilteredAccess { + fn clone(&self) -> Self { Self { - access: Access::default(), - required: FixedBitSet::default(), - filter_sets: vec![AccessFilters::default()], + access: self.access.clone(), + required: self.required.clone(), + filter_sets: self.filter_sets.clone(), } } + + fn clone_from(&mut self, source: &Self) { + self.access.clone_from(&source.access); + self.required.clone_from(&source.required); + self.filter_sets.clone_from(&source.filter_sets); + } +} + +impl Default for FilteredAccess { + fn default() -> Self { + Self::matches_everything() + } } impl From> for FilteredAccessSet { @@ -352,7 +639,74 @@ impl From> for FilteredAccessSet { } } +/// Records how two accesses conflict with each other +#[derive(Debug, PartialEq)] +pub enum AccessConflicts { + /// Conflict is for all indices + All, + /// There is a conflict for a subset of indices + Individual(FixedBitSet), +} + +impl AccessConflicts { + fn add(&mut self, other: &Self) { + match (self, other) { + (s, AccessConflicts::All) => { + *s = AccessConflicts::All; + } + (AccessConflicts::Individual(this), AccessConflicts::Individual(other)) => { + this.extend(other.ones()); + } + _ => {} + } + } + + pub(crate) fn is_empty(&self) -> bool { + match self { + Self::All => false, + Self::Individual(set) => set.is_empty(), + } + } + + /// An [`AccessConflicts`] which represents the absence of any conflict + pub(crate) fn empty() -> Self { + Self::Individual(FixedBitSet::new()) + } +} + +impl From for AccessConflicts { + fn from(value: FixedBitSet) -> Self { + Self::Individual(value) + } +} + +impl From> for AccessConflicts { + fn from(value: Vec) -> Self { + Self::Individual(value.iter().map(T::sparse_set_index).collect()) + } +} + impl FilteredAccess { + /// Returns a `FilteredAccess` which has no access and matches everything. + /// This is the equivalent of a `TRUE` logic atom. + pub fn matches_everything() -> Self { + Self { + access: Access::default(), + required: FixedBitSet::default(), + filter_sets: vec![AccessFilters::default()], + } + } + + /// Returns a `FilteredAccess` which has no access and matches nothing. + /// This is the equivalent of a `FALSE` logic atom. + pub fn matches_nothing() -> Self { + Self { + access: Access::default(), + required: FixedBitSet::default(), + filter_sets: Vec::new(), + } + } + /// Returns a reference to the underlying unfiltered access. #[inline] pub fn access(&self) -> &Access { @@ -365,20 +719,30 @@ impl FilteredAccess { &mut self.access } - /// Adds access to the element given by `index`. - pub fn add_read(&mut self, index: T) { - self.access.add_read(index.clone()); + /// Adds access to the component given by `index`. + pub fn add_component_read(&mut self, index: T) { + self.access.add_component_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()); + /// Adds exclusive access to the component given by `index`. + pub fn add_component_write(&mut self, index: T) { + self.access.add_component_write(index.clone()); self.add_required(index.clone()); self.and_with(index); } + /// Adds access to the resource given by `index`. + pub fn add_resource_read(&mut self, index: T) { + self.access.add_resource_read(index.clone()); + } + + /// Adds exclusive access to the resource given by `index`. + pub fn add_resource_write(&mut self, index: T) { + self.access.add_resource_write(index.clone()); + } + fn add_required(&mut self, index: T) { self.required.grow_and_insert(index.sparse_set_index()); } @@ -439,12 +803,12 @@ impl FilteredAccess { } /// Returns a vector of elements that this and `other` cannot access at the same time. - pub fn get_conflicts(&self, other: &FilteredAccess) -> Vec { + pub fn get_conflicts(&self, other: &FilteredAccess) -> AccessConflicts { if !self.is_compatible(other) { // filters are disjoint, so we can just look at the unfiltered intersection return self.access.get_conflicts(&other.access); } - Vec::new() + AccessConflicts::empty() } /// Adds all access and filters from `other`. @@ -489,6 +853,16 @@ impl FilteredAccess { self.access.write_all(); } + /// Sets the underlying unfiltered access as having access to all components. + pub fn read_all_components(&mut self) { + self.access.read_all_components(); + } + + /// Sets the underlying unfiltered access as having mutable access to all components. + pub fn write_all_components(&mut self) { + self.access.write_all_components(); + } + /// 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 { @@ -510,14 +884,30 @@ impl FilteredAccess { } } -#[derive(Clone, Eq, PartialEq)] +#[derive(Eq, PartialEq)] pub(crate) struct AccessFilters { pub(crate) with: FixedBitSet, pub(crate) without: FixedBitSet, _index_type: PhantomData, } -impl fmt::Debug for AccessFilters { +// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. +impl Clone for AccessFilters { + fn clone(&self) -> Self { + Self { + with: self.with.clone(), + without: self.without.clone(), + _index_type: PhantomData, + } + } + + fn clone_from(&mut self, source: &Self) { + self.with.clone_from(&source.with); + self.without.clone_from(&source.without); + } +} + +impl Debug for AccessFilters { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AccessFilters") .field("with", &FormattedBitSet::::new(&self.with)) @@ -554,12 +944,27 @@ impl AccessFilters { /// It stores multiple sets of accesses. /// - A "combined" set, which is the access of all filters in this set combined. /// - The set of access of each individual filters in this set. -#[derive(Debug, Clone)] +#[derive(Debug, PartialEq, Eq)] pub struct FilteredAccessSet { combined_access: Access, filtered_accesses: Vec>, } +// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`. +impl Clone for FilteredAccessSet { + fn clone(&self) -> Self { + Self { + combined_access: self.combined_access.clone(), + filtered_accesses: self.filtered_accesses.clone(), + } + } + + fn clone_from(&mut self, source: &Self) { + self.combined_access.clone_from(&source.combined_access); + self.filtered_accesses.clone_from(&source.filtered_accesses); + } +} + impl FilteredAccessSet { /// Returns a reference to the unfiltered access of the entire set. #[inline] @@ -594,29 +999,29 @@ impl FilteredAccessSet { } /// Returns a vector of elements that this set and `other` cannot access at the same time. - pub fn get_conflicts(&self, other: &FilteredAccessSet) -> Vec { + pub fn get_conflicts(&self, other: &FilteredAccessSet) -> AccessConflicts { // if the unfiltered access is incompatible, must check each pair - let mut conflicts = HashSet::new(); + let mut conflicts = AccessConflicts::empty(); if !self.combined_access.is_compatible(other.combined_access()) { for filtered in &self.filtered_accesses { for other_filtered in &other.filtered_accesses { - conflicts.extend(filtered.get_conflicts(other_filtered).into_iter()); + conflicts.add(&filtered.get_conflicts(other_filtered)); } } } - conflicts.into_iter().collect() + conflicts } /// Returns a vector of elements that this set and `other` cannot access at the same time. - pub fn get_conflicts_single(&self, filtered_access: &FilteredAccess) -> Vec { + pub fn get_conflicts_single(&self, filtered_access: &FilteredAccess) -> AccessConflicts { // if the unfiltered access is incompatible, must check each pair - let mut conflicts = HashSet::new(); + let mut conflicts = AccessConflicts::empty(); if !self.combined_access.is_compatible(filtered_access.access()) { for filtered in &self.filtered_accesses { - conflicts.extend(filtered.get_conflicts(filtered_access).into_iter()); + conflicts.add(&filtered.get_conflicts(filtered_access)); } } - conflicts.into_iter().collect() + conflicts } /// Adds the filtered access to the set. @@ -625,17 +1030,17 @@ impl FilteredAccessSet { self.filtered_accesses.push(filtered_access); } - /// Adds a read access without filters to the set. - pub(crate) fn add_unfiltered_read(&mut self, index: T) { + /// Adds a read access to a resource to the set. + pub(crate) fn add_unfiltered_resource_read(&mut self, index: T) { let mut filter = FilteredAccess::default(); - filter.add_read(index); + filter.add_resource_read(index); self.add(filter); } - /// Adds a write access without filters to the set. - pub(crate) fn add_unfiltered_write(&mut self, index: T) { + /// Adds a write access to a resource to the set. + pub(crate) fn add_unfiltered_resource_write(&mut self, index: T) { let mut filter = FilteredAccess::default(); - filter.add_write(index); + filter.add_resource_write(index); self.add(filter); } @@ -676,15 +1081,145 @@ impl Default for FilteredAccessSet { #[cfg(test)] mod tests { use crate::query::access::AccessFilters; - use crate::query::{Access, FilteredAccess, FilteredAccessSet}; + use crate::query::{Access, AccessConflicts, FilteredAccess, FilteredAccessSet}; use fixedbitset::FixedBitSet; use std::marker::PhantomData; + fn create_sample_access() -> Access { + let mut access = Access::::default(); + + access.add_component_read(1); + access.add_component_read(2); + access.add_component_write(3); + access.add_archetypal(5); + access.read_all(); + + access + } + + fn create_sample_filtered_access() -> FilteredAccess { + let mut filtered_access = FilteredAccess::::default(); + + filtered_access.add_component_write(1); + filtered_access.add_component_read(2); + filtered_access.add_required(3); + filtered_access.and_with(4); + + filtered_access + } + + fn create_sample_access_filters() -> AccessFilters { + let mut access_filters = AccessFilters::::default(); + + access_filters.with.grow_and_insert(3); + access_filters.without.grow_and_insert(5); + + access_filters + } + + fn create_sample_filtered_access_set() -> FilteredAccessSet { + let mut filtered_access_set = FilteredAccessSet::::default(); + + filtered_access_set.add_unfiltered_resource_read(2); + filtered_access_set.add_unfiltered_resource_write(4); + filtered_access_set.read_all(); + + filtered_access_set + } + + #[test] + fn test_access_clone() { + let original: Access = create_sample_access(); + let cloned = original.clone(); + + assert_eq!(original, cloned); + } + + #[test] + fn test_access_clone_from() { + let original: Access = create_sample_access(); + let mut cloned = Access::::default(); + + cloned.add_component_write(7); + cloned.add_component_read(4); + cloned.add_archetypal(8); + cloned.write_all(); + + cloned.clone_from(&original); + + assert_eq!(original, cloned); + } + + #[test] + fn test_filtered_access_clone() { + let original: FilteredAccess = create_sample_filtered_access(); + let cloned = original.clone(); + + assert_eq!(original, cloned); + } + + #[test] + fn test_filtered_access_clone_from() { + let original: FilteredAccess = create_sample_filtered_access(); + let mut cloned = FilteredAccess::::default(); + + cloned.add_component_write(7); + cloned.add_component_read(4); + cloned.append_or(&FilteredAccess::default()); + + cloned.clone_from(&original); + + assert_eq!(original, cloned); + } + + #[test] + fn test_access_filters_clone() { + let original: AccessFilters = create_sample_access_filters(); + let cloned = original.clone(); + + assert_eq!(original, cloned); + } + + #[test] + fn test_access_filters_clone_from() { + let original: AccessFilters = create_sample_access_filters(); + let mut cloned = AccessFilters::::default(); + + cloned.with.grow_and_insert(1); + cloned.without.grow_and_insert(2); + + cloned.clone_from(&original); + + assert_eq!(original, cloned); + } + + #[test] + fn test_filtered_access_set_clone() { + let original: FilteredAccessSet = create_sample_filtered_access_set(); + let cloned = original.clone(); + + assert_eq!(original, cloned); + } + + #[test] + fn test_filtered_access_set_from() { + let original: FilteredAccessSet = create_sample_filtered_access_set(); + let mut cloned = FilteredAccessSet::::default(); + + cloned.add_unfiltered_resource_read(7); + cloned.add_unfiltered_resource_write(9); + cloned.write_all(); + + cloned.clone_from(&original); + + assert_eq!(original, cloned); + } + #[test] fn read_all_access_conflicts() { // read_all / single write let mut access_a = Access::::default(); - access_a.add_write(0); + access_a.add_component_write(0); let mut access_b = Access::::default(); access_b.read_all(); @@ -704,42 +1239,48 @@ mod tests { #[test] fn access_get_conflicts() { let mut access_a = Access::::default(); - access_a.add_read(0); - access_a.add_read(1); + access_a.add_component_read(0); + access_a.add_component_read(1); let mut access_b = Access::::default(); - access_b.add_read(0); - access_b.add_write(1); + access_b.add_component_read(0); + access_b.add_component_write(1); - assert_eq!(access_a.get_conflicts(&access_b), vec![1]); + assert_eq!(access_a.get_conflicts(&access_b), vec![1_usize].into()); let mut access_c = Access::::default(); - access_c.add_write(0); - access_c.add_write(1); + access_c.add_component_write(0); + access_c.add_component_write(1); - assert_eq!(access_a.get_conflicts(&access_c), vec![0, 1]); - assert_eq!(access_b.get_conflicts(&access_c), vec![0, 1]); + assert_eq!( + access_a.get_conflicts(&access_c), + vec![0_usize, 1_usize].into() + ); + assert_eq!( + access_b.get_conflicts(&access_c), + vec![0_usize, 1_usize].into() + ); let mut access_d = Access::::default(); - access_d.add_read(0); + access_d.add_component_read(0); - assert_eq!(access_d.get_conflicts(&access_a), vec![]); - assert_eq!(access_d.get_conflicts(&access_b), vec![]); - assert_eq!(access_d.get_conflicts(&access_c), vec![0]); + assert_eq!(access_d.get_conflicts(&access_a), AccessConflicts::empty()); + assert_eq!(access_d.get_conflicts(&access_b), AccessConflicts::empty()); + assert_eq!(access_d.get_conflicts(&access_c), vec![0_usize].into()); } #[test] fn filtered_combined_access() { let mut access_a = FilteredAccessSet::::default(); - access_a.add_unfiltered_read(1); + access_a.add_unfiltered_resource_read(1); let mut filter_b = FilteredAccess::::default(); - filter_b.add_write(1); + filter_b.add_resource_write(1); let conflicts = access_a.get_conflicts_single(&filter_b); assert_eq!( &conflicts, - &[1_usize], + &AccessConflicts::from(vec![1_usize]), "access_a: {access_a:?}, filter_b: {filter_b:?}" ); } @@ -747,22 +1288,22 @@ mod tests { #[test] fn filtered_access_extend() { let mut access_a = FilteredAccess::::default(); - access_a.add_read(0); - access_a.add_read(1); + access_a.add_component_read(0); + access_a.add_component_read(1); access_a.and_with(2); let mut access_b = FilteredAccess::::default(); - access_b.add_read(0); - access_b.add_write(3); + access_b.add_component_read(0); + access_b.add_component_write(3); access_b.and_without(4); access_a.extend(&access_b); let mut expected = FilteredAccess::::default(); - expected.add_read(0); - expected.add_read(1); + expected.add_component_read(0); + expected.add_component_read(1); expected.and_with(2); - expected.add_write(3); + expected.add_component_write(3); expected.and_without(4); assert!(access_a.eq(&expected)); @@ -772,8 +1313,8 @@ mod tests { fn filtered_access_extend_or() { let mut access_a = FilteredAccess::::default(); // Exclusive access to `(&mut A, &mut B)`. - access_a.add_write(0); - access_a.add_write(1); + access_a.add_component_write(0); + access_a.add_component_write(1); // Filter by `With`. let mut access_b = FilteredAccess::::default(); @@ -794,8 +1335,8 @@ mod tests { // The intention here is to test that exclusive access implied by `add_write` // forms correct normalized access structs when extended with `Or` filters. let mut expected = FilteredAccess::::default(); - expected.add_write(0); - expected.add_write(1); + expected.add_component_write(0); + expected.add_component_write(1); // The resulted access is expected to represent `Or<((With, With, With), (With, With, With, Without))>`. expected.filter_sets = vec![ AccessFilters { diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index 101371d00400fc..3076b5d54e0905 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -142,14 +142,14 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { /// 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.access.add_component_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.access.add_component_write(id); self } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 08bcbbe27bf25c..8e522b61a2a7a3 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,6 +1,6 @@ use crate::{ archetype::{Archetype, Archetypes}, - change_detection::{Ticks, TicksMut}, + change_detection::{MaybeThinSlicePtrLocation, Ticks, TicksMut}, component::{Component, ComponentId, Components, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, @@ -301,6 +301,8 @@ unsafe impl WorldQuery for Entity { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + unsafe fn init_fetch<'w>( _world: UnsafeWorldCell<'w>, _state: &Self::State, @@ -369,6 +371,10 @@ unsafe impl WorldQuery for EntityLocation { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, @@ -442,6 +448,10 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, @@ -480,10 +490,10 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { assert!( - !access.access().has_any_write(), + !access.access().has_any_component_write(), "EntityRef conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", ); - access.read_all(); + access.read_all_components(); } fn init_state(_world: &mut World) {} @@ -518,6 +528,10 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, @@ -556,10 +570,10 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { assert!( - !access.access().has_any_read(), + !access.access().has_any_component_read(), "EntityMut conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.", ); - access.write_all(); + access.write_all_components(); } fn init_state(_world: &mut World) {} @@ -591,6 +605,10 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + const IS_DENSE: bool = false; unsafe fn init_fetch<'w>( @@ -600,7 +618,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { _this_run: Tick, ) -> Self::Fetch<'w> { let mut access = Access::default(); - access.read_all(); + access.read_all_components(); (world, access) } @@ -612,9 +630,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { _table: &Table, ) { let mut access = Access::default(); - state.access.reads().for_each(|id| { + state.access.component_reads().for_each(|id| { if archetype.contains(id) { - access.add_read(id); + access.add_component_read(id); } }); fetch.1 = access; @@ -623,9 +641,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { #[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| { + state.access.component_reads().for_each(|id| { if table.has_column(id) { - access.add_read(id); + access.add_component_read(id); } }); fetch.1 = access; @@ -633,7 +651,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { #[inline] fn set_access<'w>(state: &mut Self::State, access: &FilteredAccess) { - *state = access.clone(); + state.clone_from(access); state.access_mut().clear_writes(); } @@ -694,6 +712,10 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + const IS_DENSE: bool = false; unsafe fn init_fetch<'w>( @@ -703,7 +725,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { _this_run: Tick, ) -> Self::Fetch<'w> { let mut access = Access::default(); - access.write_all(); + access.write_all_components(); (world, access) } @@ -715,14 +737,14 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { _table: &Table, ) { let mut access = Access::default(); - state.access.reads().for_each(|id| { + state.access.component_reads().for_each(|id| { if archetype.contains(id) { - access.add_read(id); + access.add_component_read(id); } }); - state.access.writes().for_each(|id| { + state.access.component_writes().for_each(|id| { if archetype.contains(id) { - access.add_write(id); + access.add_component_write(id); } }); fetch.1 = access; @@ -731,14 +753,14 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { #[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| { + state.access.component_reads().for_each(|id| { if table.has_column(id) { - access.add_read(id); + access.add_component_read(id); } }); - state.access.writes().for_each(|id| { + state.access.component_writes().for_each(|id| { if table.has_column(id) { - access.add_write(id); + access.add_component_write(id); } }); fetch.1 = access; @@ -746,7 +768,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { #[inline] fn set_access<'w>(state: &mut Self::State, access: &FilteredAccess) { - *state = access.clone(); + state.clone_from(access); } #[inline(always)] @@ -805,6 +827,10 @@ unsafe impl WorldQuery for &Archetype { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, _state: &Self::State, @@ -897,6 +923,10 @@ unsafe impl WorldQuery for &T { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -988,11 +1018,11 @@ unsafe impl WorldQuery for &T { access: &mut FilteredAccess, ) { assert!( - !access.access().has_write(component_id), + !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - std::any::type_name::(), + std::any::type_name::(), ); - access.add_read(component_id); + access.add_component_read(component_id); } fn init_state(world: &mut World) -> ComponentId { @@ -1026,6 +1056,7 @@ pub struct RefFetch<'w, T> { ThinSlicePtr<'w, UnsafeCell>, ThinSlicePtr<'w, UnsafeCell>, ThinSlicePtr<'w, UnsafeCell>, + MaybeThinSlicePtrLocation<'w>, )>, // T::STORAGE_TYPE = StorageType::SparseSet sparse_set: Option<&'w ComponentSparseSet>, @@ -1055,6 +1086,10 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -1115,6 +1150,10 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { column.get_data_slice().into(), column.get_added_ticks_slice().into(), column.get_changed_ticks_slice().into(), + #[cfg(feature = "track_change_detection")] + column.get_changed_by_slice().into(), + #[cfg(not(feature = "track_change_detection"))] + (), )); } @@ -1127,7 +1166,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { match T::STORAGE_TYPE { StorageType::Table => { // SAFETY: STORAGE_TYPE = Table - let (table_components, added_ticks, changed_ticks) = + let (table_components, added_ticks, changed_ticks, _callers) = unsafe { fetch.table_data.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. @@ -1136,6 +1175,9 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { let added = unsafe { added_ticks.get(table_row.as_usize()) }; // SAFETY: The caller ensures `table_row` is in range. let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; + // SAFETY: The caller ensures `table_row` is in range. + #[cfg(feature = "track_change_detection")] + let caller = unsafe { _callers.get(table_row.as_usize()) }; Ref { value: component.deref(), @@ -1145,6 +1187,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { this_run: fetch.this_run, last_run: fetch.last_run, }, + #[cfg(feature = "track_change_detection")] + changed_by: caller.deref(), } } StorageType::SparseSet => { @@ -1152,7 +1196,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { let component_sparse_set = unsafe { fetch.sparse_set.debug_checked_unwrap() }; // SAFETY: The caller ensures `entity` is in range. - let (component, ticks) = unsafe { + let (component, ticks, _caller) = unsafe { component_sparse_set .get_with_ticks(entity) .debug_checked_unwrap() @@ -1161,6 +1205,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { Ref { value: component.deref(), ticks: Ticks::from_tick_cells(ticks, fetch.last_run, fetch.this_run), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref(), } } } @@ -1171,11 +1217,11 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { access: &mut FilteredAccess, ) { assert!( - !access.access().has_write(component_id), + !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - std::any::type_name::(), + std::any::type_name::(), ); - access.add_read(component_id); + access.add_component_read(component_id); } fn init_state(world: &mut World) -> ComponentId { @@ -1209,6 +1255,7 @@ pub struct WriteFetch<'w, T> { ThinSlicePtr<'w, UnsafeCell>, ThinSlicePtr<'w, UnsafeCell>, ThinSlicePtr<'w, UnsafeCell>, + MaybeThinSlicePtrLocation<'w>, )>, // T::STORAGE_TYPE = StorageType::SparseSet sparse_set: Option<&'w ComponentSparseSet>, @@ -1238,6 +1285,10 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -1298,6 +1349,10 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { column.get_data_slice().into(), column.get_added_ticks_slice().into(), column.get_changed_ticks_slice().into(), + #[cfg(feature = "track_change_detection")] + column.get_changed_by_slice().into(), + #[cfg(not(feature = "track_change_detection"))] + (), )); } @@ -1310,7 +1365,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { match T::STORAGE_TYPE { StorageType::Table => { // SAFETY: STORAGE_TYPE = Table - let (table_components, added_ticks, changed_ticks) = + let (table_components, added_ticks, changed_ticks, _callers) = unsafe { fetch.table_data.debug_checked_unwrap() }; // SAFETY: The caller ensures `table_row` is in range. @@ -1319,6 +1374,9 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { let added = unsafe { added_ticks.get(table_row.as_usize()) }; // SAFETY: The caller ensures `table_row` is in range. let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; + // SAFETY: The caller ensures `table_row` is in range. + #[cfg(feature = "track_change_detection")] + let caller = unsafe { _callers.get(table_row.as_usize()) }; Mut { value: component.deref_mut(), @@ -1328,6 +1386,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { this_run: fetch.this_run, last_run: fetch.last_run, }, + #[cfg(feature = "track_change_detection")] + changed_by: caller.deref_mut(), } } StorageType::SparseSet => { @@ -1335,7 +1395,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { let component_sparse_set = unsafe { fetch.sparse_set.debug_checked_unwrap() }; // SAFETY: The caller ensures `entity` is in range. - let (component, ticks) = unsafe { + let (component, ticks, _caller) = unsafe { component_sparse_set .get_with_ticks(entity) .debug_checked_unwrap() @@ -1344,6 +1404,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { Mut { value: component.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, fetch.last_run, fetch.this_run), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref_mut(), } } } @@ -1354,11 +1416,11 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { access: &mut FilteredAccess, ) { assert!( - !access.access().has_read(component_id), + !access.access().has_component_read(component_id), "&mut {} conflicts with a previous access in this query. Mutable component access must be unique.", - std::any::type_name::(), + std::any::type_name::(), ); - access.add_write(component_id); + access.add_component_write(component_id); } fn init_state(world: &mut World) -> ComponentId { @@ -1401,6 +1463,10 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { <&mut T as WorldQuery>::shrink(item) } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] // Forwarded to `&mut T` unsafe fn init_fetch<'w>( @@ -1452,11 +1518,11 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { // Update component access here instead of in `<&mut T as WorldQuery>` to avoid erroneously referencing // `&mut T` in error message. assert!( - !access.access().has_read(component_id), + !access.access().has_component_read(component_id), "Mut<{}> conflicts with a previous access in this query. Mutable component access mut be unique.", - std::any::type_name::(), + std::any::type_name::(), ); - access.add_write(component_id); + access.add_component_write(component_id); } // Forwarded to `&mut T` @@ -1511,6 +1577,13 @@ unsafe impl WorldQuery for Option { item.map(T::shrink) } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + OptionFetch { + fetch: T::shrink_fetch(fetch.fetch), + matches: fetch.matches, + } + } + #[inline] unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -1688,6 +1761,10 @@ unsafe impl WorldQuery for Has { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] unsafe fn init_fetch<'w>( _world: UnsafeWorldCell<'w>, @@ -1769,10 +1846,11 @@ unsafe impl ReadOnlyQueryData for Has {} pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { - ($(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { #[allow(non_snake_case)] #[allow(clippy::unused_unit)] + $(#[$meta])* // SAFETY: defers to soundness `$name: WorldQuery` impl unsafe impl<$($name: QueryData),*> QueryData for ($($name,)*) { type ReadOnly = ($($name::ReadOnly,)*); @@ -1805,6 +1883,12 @@ macro_rules! impl_anytuple_fetch { $name.map($name::shrink), )*) } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + let ($($name,)*) = fetch; + ($( + ($name::shrink_fetch($name.0), $name.1), + )*) + } #[inline] #[allow(clippy::unused_unit)] @@ -1861,30 +1945,30 @@ macro_rules! impl_anytuple_fetch { )*) } - fn update_component_access(state: &Self::State, _access: &mut FilteredAccess) { - let mut _new_access = _access.clone(); - + fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { // update the filters (Or<(With<$name>,)>) let ($($name,)*) = state; - let mut _not_first = false; + + let mut _new_access = FilteredAccess::matches_nothing(); + $( - if _not_first { - // we use an intermediate access because we only want to update the filter_sets, not the access - let mut intermediate = _access.clone(); - $name::update_component_access($name, &mut intermediate); - _new_access.append_or(&intermediate); - } else { - $name::update_component_access($name, &mut _new_access); - _new_access.required = _access.required.clone(); - _not_first = true; - } + // Create an intermediate because `access`'s value needs to be preserved + // for the next query data, and `_new_access` has to be modified only by `append_or` to it, + // which only updates the `filter_sets`, not the `access`. + let mut intermediate = access.clone(); + $name::update_component_access($name, &mut intermediate); + _new_access.append_or(&intermediate); )* - _access.filter_sets = _new_access.filter_sets; + // Of the accumulated `_new_access` we only care about the filter sets, not the access. + access.filter_sets = _new_access.filter_sets; - // update the access (add the read/writes) - // Option updates the access but not the filter_sets - <($(Option<$name>,)*)>::update_component_access(state, _access); + // For the access we instead delegate to a tuple of `Option`s. + // This has essentially the same semantics of `AnyOf`, except that it doesn't + // require at least one of them to be `Some`. + // We however solve this by setting explicitly the `filter_sets` above. + // Also note that Option updates the `access` but not the `filter_sets`. + <($(Option<$name>,)*)>::update_component_access(state, access); } #[allow(unused_variables)] @@ -1914,7 +1998,14 @@ macro_rules! impl_anytuple_fetch { }; } -all_tuples!(impl_tuple_query_data, 0, 15, F, S); +all_tuples!( + #[doc(fake_variadic)] + impl_tuple_query_data, + 0, + 15, + F, + S +); all_tuples!(impl_anytuple_fetch, 0, 15, F, S); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` @@ -1932,6 +2023,8 @@ unsafe impl WorldQuery for NopWorldQuery { fn shrink<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: ()) {} + #[inline(always)] unsafe fn init_fetch( _world: UnsafeWorldCell, @@ -2000,6 +2093,9 @@ unsafe impl WorldQuery for PhantomData { fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + } + unsafe fn init_fetch<'w>( _world: UnsafeWorldCell<'w>, _state: &Self::State, diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 316aed862bc44f..affdaf2828b63a 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -142,6 +142,8 @@ unsafe impl WorldQuery for With { fn shrink<'wlong: 'wshort, 'wshort>(_: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + #[inline] unsafe fn init_fetch( _world: UnsafeWorldCell, @@ -250,6 +252,8 @@ unsafe impl WorldQuery for Without { fn shrink<'wlong: 'wshort, 'wshort>(_: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} + #[inline] unsafe fn init_fetch( _world: UnsafeWorldCell, @@ -386,6 +390,16 @@ macro_rules! impl_or_query_filter { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + let ($($filter,)*) = fetch; + ($( + OrFetch { + fetch: $filter::shrink_fetch($filter.fetch), + matches: $filter.matches + }, + )*) + } + const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; #[inline] @@ -443,21 +457,22 @@ macro_rules! impl_or_query_filter { fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { let ($($filter,)*) = state; - let mut _new_access = access.clone(); - let mut _not_first = false; + let mut _new_access = FilteredAccess::matches_nothing(); + $( - if _not_first { - let mut intermediate = access.clone(); - $filter::update_component_access($filter, &mut intermediate); - _new_access.append_or(&intermediate); - _new_access.extend_access(&intermediate); - } else { - $filter::update_component_access($filter, &mut _new_access); - _new_access.required = access.required.clone(); - _not_first = true; - } + // Create an intermediate because `access`'s value needs to be preserved + // for the next filter, and `_new_access` has to be modified only by `append_or` to it. + let mut intermediate = access.clone(); + $filter::update_component_access($filter, &mut intermediate); + _new_access.append_or(&intermediate); + // Also extend the accesses required to compute the filter. This is required because + // otherwise a `Query<(), Or<(Changed,)>` won't conflict with `Query<&mut Foo>`. + _new_access.extend_access(&intermediate); )* + // The required components remain the same as the original `access`. + _new_access.required = std::mem::take(&mut access.required); + *access = _new_access; } @@ -492,11 +507,11 @@ macro_rules! impl_or_query_filter { } macro_rules! impl_tuple_query_filter { - ($($name: ident),*) => { + ($(#[$meta:meta])* $($name: ident),*) => { #[allow(unused_variables)] #[allow(non_snake_case)] #[allow(clippy::unused_unit)] - + $(#[$meta])* impl<$($name: QueryFilter),*> QueryFilter for ($($name,)*) { const IS_ARCHETYPAL: bool = true $(&& $name::IS_ARCHETYPAL)*; @@ -515,7 +530,13 @@ macro_rules! impl_tuple_query_filter { }; } -all_tuples!(impl_tuple_query_filter, 0, 15, F); +all_tuples!( + #[doc(fake_variadic)] + impl_tuple_query_filter, + 0, + 15, + F +); all_tuples!(impl_or_query_filter, 0, 15, F, S); /// A filter on a component that only retains results the first time after they have been added. @@ -607,6 +628,10 @@ unsafe impl WorldQuery for Added { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -687,10 +712,10 @@ unsafe impl WorldQuery for Added { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { - if access.access().has_write(id) { + if access.access().has_component_write(id) { panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",std::any::type_name::()); } - access.add_read(id); + access.add_component_read(id); } fn init_state(world: &mut World) -> ComponentId { @@ -818,6 +843,10 @@ unsafe impl WorldQuery for Changed { item } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + fetch + } + #[inline] unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, @@ -898,10 +927,10 @@ unsafe impl WorldQuery for Changed { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { - if access.access().has_write(id) { + if access.access().has_component_write(id) { panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",std::any::type_name::()); } - access.add_read(id); + access.add_component_read(id); } fn init_state(world: &mut World) -> ComponentId { @@ -957,11 +986,24 @@ impl ArchetypeFilter for With {} impl ArchetypeFilter for Without {} macro_rules! impl_archetype_filter_tuple { - ($($filter: ident),*) => { + ($(#[$meta:meta])* $($filter: ident),*) => { + $(#[$meta])* impl<$($filter: ArchetypeFilter),*> ArchetypeFilter for ($($filter,)*) {} + }; +} +macro_rules! impl_archetype_or_filter_tuple { + ($($filter: ident),*) => { impl<$($filter: ArchetypeFilter),*> ArchetypeFilter for Or<($($filter,)*)> {} }; } -all_tuples!(impl_archetype_filter_tuple, 0, 15, F); +all_tuples!( + #[doc(fake_variadic)] + impl_archetype_filter_tuple, + 0, + 15, + F +); + +all_tuples!(impl_archetype_or_filter_tuple, 0, 15, F); diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 05955d5b378543..f8564025aa15c8 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -50,6 +50,78 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } } + /// Creates a new separate iterator yielding the same remaining items of the current one. + /// Advancing the new iterator will not advance the original one, which will resume at the + /// point it was left at. + /// + /// Differently from [`remaining_mut`](QueryIter::remaining_mut) the new iterator does not + /// borrow from the original one. However it can only be called from an iterator over read only + /// items. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct ComponentA; + /// + /// fn combinations(query: Query<&ComponentA>) { + /// let mut iter = query.iter(); + /// while let Some(a) = iter.next() { + /// for b in iter.remaining() { + /// // Check every combination (a, b) + /// } + /// } + /// } + /// ``` + pub fn remaining(&self) -> QueryIter<'w, 's, D, F> + where + D: ReadOnlyQueryData, + { + QueryIter { + world: self.world, + tables: self.tables, + archetypes: self.archetypes, + query_state: self.query_state, + cursor: self.cursor.clone(), + } + } + + /// Creates a new separate iterator yielding the same remaining items of the current one. + /// Advancing the new iterator will not advance the original one, which will resume at the + /// point it was left at. + /// + /// This method can be called on iterators over mutable items. However the original iterator + /// will be borrowed while the new iterator exists and will thus not be usable in that timespan. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct ComponentA; + /// + /// fn combinations(mut query: Query<&mut ComponentA>) { + /// let mut iter = query.iter_mut(); + /// while let Some(a) = iter.next() { + /// for b in iter.remaining_mut() { + /// // Check every combination (a, b) + /// } + /// } + /// } + /// ``` + pub fn remaining_mut(&mut self) -> QueryIter<'_, 's, D, F> { + QueryIter { + world: self.world, + tables: self.tables, + archetypes: self.archetypes, + query_state: self.query_state, + cursor: self.cursor.reborrow(), + } + } + /// Executes the equivalent of [`Iterator::fold`] over a contiguous segment /// from an table. /// @@ -68,6 +140,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { where Func: FnMut(B, D::Item<'w>) -> B, { + if table.is_empty() { + return accum; + } assert!( rows.end <= u32::MAX as usize, "TableRow is only valid up to u32::MAX" @@ -120,6 +195,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { where Func: FnMut(B, D::Item<'w>) -> B, { + if archetype.is_empty() { + return accum; + } let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); D::set_archetype( &mut self.cursor.fetch, @@ -167,6 +245,73 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { accum } + /// Executes the equivalent of [`Iterator::fold`] over a contiguous segment + /// from an archetype which has the same entity count as its table. + /// + /// # Safety + /// - all `indices` must be in `[0, archetype.len())`. + /// - `archetype` must match D and F + /// - `archetype` must have the same length with it's table. + /// - Either `D::IS_DENSE` or `F::IS_DENSE` must be false. + #[inline] + pub(super) unsafe fn fold_over_dense_archetype_range( + &mut self, + mut accum: B, + func: &mut Func, + archetype: &'w Archetype, + rows: Range, + ) -> B + where + Func: FnMut(B, D::Item<'w>) -> B, + { + if archetype.is_empty() { + return accum; + } + assert!( + rows.end <= u32::MAX as usize, + "TableRow is only valid up to u32::MAX" + ); + let table = self.tables.get(archetype.table_id()).debug_checked_unwrap(); + + debug_assert!( + archetype.len() == table.entity_count(), + "archetype and it's table must have the same length. " + ); + + D::set_archetype( + &mut self.cursor.fetch, + &self.query_state.fetch_state, + archetype, + table, + ); + F::set_archetype( + &mut self.cursor.filter, + &self.query_state.filter_state, + archetype, + table, + ); + let entities = table.entities(); + for row in rows { + // SAFETY: Caller assures `row` in range of the current archetype. + let entity = unsafe { *entities.get_unchecked(row) }; + let row = TableRow::from_usize(row); + + // SAFETY: set_table was called prior. + // Caller assures `row` in range of the current archetype. + let filter_matched = unsafe { F::filter_fetch(&mut self.cursor.filter, entity, row) }; + if !filter_matched { + continue; + } + + // SAFETY: set_table was called prior. + // Caller assures `row` in range of the current archetype. + let item = D::fetch(&mut self.cursor.fetch, entity, row); + + accum = func(accum, item); + } + accum + } + /// Sorts all query items into a new iterator, using the query lens as a key. /// /// This sort is stable (i.e., does not reorder equal elements). @@ -298,9 +443,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -392,9 +535,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -494,9 +635,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -562,9 +701,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -693,9 +830,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -764,9 +899,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -836,9 +969,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { let world = self.world; - let query_lens_state = self - .query_state - .transmute_filtered::<(L, Entity), F>(world.components()); + let query_lens_state = self.query_state.transmute_filtered::<(L, Entity), F>(world); // SAFETY: // `self.world` has permission to access the required components. @@ -914,12 +1045,27 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> let archetype = // SAFETY: Matched archetype IDs are guaranteed to still exist. unsafe { self.archetypes.get(id.archetype_id).debug_checked_unwrap() }; - accum = + // SAFETY: Matched table IDs are guaranteed to still exist. + let table = unsafe { self.tables.get(archetype.table_id()).debug_checked_unwrap() }; + + // When an archetype and its table have equal entity counts, dense iteration can be safely used. + // this leverages cache locality to optimize performance. + if table.entity_count() == archetype.len() { + accum = // SAFETY: // - The fetched archetype matches both D and F + // - The provided archetype and its' table have the same length. // - The provided range is equivalent to [0, archetype.len) // - The if block ensures that ether D::IS_DENSE or F::IS_DENSE are false - unsafe { self.fold_over_archetype_range(accum, &mut func, archetype, 0..archetype.len()) }; + unsafe { self.fold_over_dense_archetype_range(accum, &mut func, archetype,0..archetype.len()) }; + } else { + accum = + // SAFETY: + // - The fetched archetype matches both D and F + // - The provided range is equivalent to [0, archetype.len) + // - The if block ensures that ether D::IS_DENSE or F::IS_DENSE are false + unsafe { self.fold_over_archetype_range(accum, &mut func, archetype,0..archetype.len()) }; + } } } accum @@ -1116,61 +1262,57 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator>> } /// Safety: + /// All arguments must stem from the same valid `QueryManyIter`. + /// /// The lifetime here is not restrictive enough for Fetch with &mut access, /// as calling `fetch_next_aliased_unchecked` multiple times can produce multiple /// references to the same component, leading to unique reference aliasing. /// /// It is always safe for shared access. #[inline(always)] - unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option> { - for entity in self.entity_iter.by_ref() { + unsafe fn fetch_next_aliased_unchecked( + entity_iter: impl Iterator>, + entities: &'w Entities, + tables: &'w Tables, + archetypes: &'w Archetypes, + fetch: &mut D::Fetch<'w>, + filter: &mut F::Fetch<'w>, + query_state: &'s QueryState, + ) -> Option> { + for entity in entity_iter { let entity = *entity.borrow(); - let Some(location) = self.entities.get(entity) else { + let Some(location) = entities.get(entity) else { continue; }; - if !self - .query_state + if !query_state .matched_archetypes .contains(location.archetype_id.index()) { continue; } - let archetype = self - .archetypes - .get(location.archetype_id) - .debug_checked_unwrap(); - let table = self.tables.get(location.table_id).debug_checked_unwrap(); + let archetype = archetypes.get(location.archetype_id).debug_checked_unwrap(); + let table = tables.get(location.table_id).debug_checked_unwrap(); // SAFETY: `archetype` is from the world that `fetch/filter` were created for, // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with unsafe { - D::set_archetype( - &mut self.fetch, - &self.query_state.fetch_state, - archetype, - table, - ); + D::set_archetype(fetch, &query_state.fetch_state, archetype, table); } // SAFETY: `table` is from the world that `fetch/filter` were created for, // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with unsafe { - F::set_archetype( - &mut self.filter, - &self.query_state.filter_state, - archetype, - table, - ); + F::set_archetype(filter, &query_state.filter_state, archetype, table); } // SAFETY: set_archetype was called prior. // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d - if unsafe { F::filter_fetch(&mut self.filter, entity, location.table_row) } { + if unsafe { F::filter_fetch(filter, entity, location.table_row) } { // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - return Some(unsafe { D::fetch(&mut self.fetch, entity, location.table_row) }); + return Some(unsafe { D::fetch(fetch, entity, location.table_row) }); } } None @@ -1179,10 +1321,49 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator>> /// Get next result from the query #[inline(always)] pub fn fetch_next(&mut self) -> Option> { - // SAFETY: we are limiting the returned reference to self, + // SAFETY: + // All arguments stem from self. + // We are limiting the returned reference to self, // making sure this method cannot be called multiple times without getting rid // of any previously returned unique references first, thus preventing aliasing. - unsafe { self.fetch_next_aliased_unchecked().map(D::shrink) } + unsafe { + Self::fetch_next_aliased_unchecked( + &mut self.entity_iter, + self.entities, + self.tables, + self.archetypes, + &mut self.fetch, + &mut self.filter, + self.query_state, + ) + .map(D::shrink) + } + } +} + +impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator>> + QueryManyIter<'w, 's, D, F, I> +{ + /// Get next result from the back of the query + #[inline(always)] + pub fn fetch_next_back(&mut self) -> Option> { + // SAFETY: + // All arguments stem from self. + // We are limiting the returned reference to self, + // making sure this method cannot be called multiple times without getting rid + // of any previously returned unique references first, thus preventing aliasing. + unsafe { + Self::fetch_next_aliased_unchecked( + self.entity_iter.by_ref().rev(), + self.entities, + self.tables, + self.archetypes, + &mut self.fetch, + &mut self.filter, + self.query_state, + ) + .map(D::shrink) + } } } @@ -1193,8 +1374,20 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator Option { - // SAFETY: It is safe to alias for ReadOnlyWorldQuery. - unsafe { self.fetch_next_aliased_unchecked() } + // SAFETY: + // All arguments stem from self. + // It is safe to alias for ReadOnlyWorldQuery. + unsafe { + Self::fetch_next_aliased_unchecked( + &mut self.entity_iter, + self.entities, + self.tables, + self.archetypes, + &mut self.fetch, + &mut self.filter, + self.query_state, + ) + } } fn size_hint(&self) -> (usize, Option) { @@ -1203,6 +1396,33 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator>, + > DoubleEndedIterator for QueryManyIter<'w, 's, D, F, I> +{ + #[inline(always)] + fn next_back(&mut self) -> Option { + // SAFETY: + // All arguments stem from self. + // It is safe to alias for ReadOnlyWorldQuery. + unsafe { + Self::fetch_next_aliased_unchecked( + self.entity_iter.by_ref().rev(), + self.entities, + self.tables, + self.archetypes, + &mut self.fetch, + &mut self.filter, + self.query_state, + ) + } + } +} + // This is correct as [`QueryManyIter`] always returns `None` once exhausted. impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator>> FusedIterator for QueryManyIter<'w, 's, D, F, I> @@ -1297,6 +1517,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< last_run: Tick, this_run: Tick, ) -> Self { + assert!(K != 0, "K should not equal to zero"); // Initialize array with cursors. // There is no FromIterator on arrays, so instead initialize it manually with MaybeUninit @@ -1304,14 +1525,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< let ptr = array .as_mut_ptr() .cast::>(); - if K != 0 { - ptr.write(QueryIterationCursor::init( - world, - query_state, - last_run, - this_run, - )); - } + ptr.write(QueryIterationCursor::init( + world, + query_state, + last_run, + this_run, + )); for slot in (1..K).map(|offset| ptr.add(offset)) { slot.write(QueryIterationCursor::init_empty( world, @@ -1336,11 +1555,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// references to the same component, leading to unique reference aliasing. ///. /// It is always safe for shared access. + #[inline] unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w>; K]> { - if K == 0 { - return None; - } - // PERF: can speed up the following code using `cursor.remaining()` instead of `next_item.is_none()` // when D::IS_ARCHETYPAL && F::IS_ARCHETYPAL // @@ -1521,6 +1737,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { } } + fn reborrow(&mut self) -> QueryIterationCursor<'_, 's, D, F> { + QueryIterationCursor { + fetch: D::shrink_fetch(self.fetch.clone()), + filter: F::shrink_fetch(self.filter.clone()), + table_entities: self.table_entities, + archetype_entities: self.archetype_entities, + storage_id_iter: self.storage_id_iter.clone(), + current_len: self.current_len, + current_row: self.current_row, + } + } + /// retrieve item returned from most recent `next` call again. #[inline] unsafe fn peek_last(&mut self) -> Option> { @@ -1581,6 +1809,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { if self.current_row == self.current_len { let table_id = self.storage_id_iter.next()?.table_id; let table = tables.get(table_id).debug_checked_unwrap(); + if table.is_empty() { + continue; + } // SAFETY: `table` is from the world that `fetch/filter` were created for, // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with unsafe { @@ -1590,7 +1821,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { self.table_entities = table.entities(); self.current_len = table.entity_count(); self.current_row = 0; - continue; } // SAFETY: set_table was called prior. @@ -1617,6 +1847,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { if self.current_row == self.current_len { let archetype_id = self.storage_id_iter.next()?.archetype_id; let archetype = archetypes.get(archetype_id).debug_checked_unwrap(); + if archetype.is_empty() { + continue; + } let table = tables.get(archetype.table_id()).debug_checked_unwrap(); // SAFETY: `archetype` and `tables` are from the world that `fetch/filter` were created for, // `fetch_state`/`filter_state` are the states that `fetch/filter` were initialized with @@ -1637,7 +1870,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { self.archetype_entities = archetype.entities(); self.current_len = archetype.len(); self.current_row = 0; - continue; } // SAFETY: set_archetype was called prior. diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 4cc2b9f3458a85..f1719d8ba52926 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -78,17 +78,19 @@ mod tests { use crate::{self as bevy_ecs, component::Component, world::World}; use std::any::type_name; use std::collections::HashSet; + use std::fmt::Debug; + use std::hash::Hash; - #[derive(Component, Debug, Hash, Eq, PartialEq, Clone, Copy)] + #[derive(Component, Debug, Hash, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] struct A(usize); - #[derive(Component, Debug, Eq, PartialEq, Clone, Copy)] + #[derive(Component, Debug, Hash, Eq, PartialEq, Clone, Copy)] struct B(usize); #[derive(Component, Debug, Eq, PartialEq, Clone, Copy)] struct C(usize); #[derive(Component, Debug, Eq, PartialEq, Clone, Copy)] struct D(usize); - #[derive(Component, Debug, Eq, PartialEq, Clone, Copy)] + #[derive(Component, Debug, Hash, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] #[component(storage = "SparseSet")] struct Sparse(usize); @@ -97,8 +99,9 @@ mod tests { let mut world = World::new(); world.spawn((A(1), B(1))); world.spawn(A(2)); - let values = world.query::<&A>().iter(&world).collect::>(); - assert_eq!(values, vec![&A(1), &A(2)]); + let values = world.query::<&A>().iter(&world).collect::>(); + assert!(values.contains(&A(1))); + assert!(values.contains(&A(2))); for (_a, mut b) in world.query::<(&A, &mut B)>().iter_mut(&mut world) { b.0 = 3; @@ -143,7 +146,6 @@ mod tests { assert_all_exact_sizes_iterator_equal(query.iter(world), expected_size, 5, query_type); let expected = expected_size; - assert_combination::(world, choose(expected, 0)); assert_combination::(world, choose(expected, 1)); assert_combination::(world, choose(expected, 2)); assert_combination::(world, choose(expected, 5)); @@ -244,6 +246,20 @@ mod tests { assert_all_sizes_equal::, With)>(&mut world, 6); } + // the order of the combinations is not guaranteed, but each unique combination is present + fn check_combinations( + values: HashSet<[&T; K]>, + expected: HashSet<[&T; K]>, + ) { + values.iter().for_each(|pair| { + let mut sorted = *pair; + sorted.sort(); + assert!(expected.contains(&sorted), + "the results of iter_combinations should contain this combination {:?}. Expected: {:?}, got: {:?}", + &sorted, &expected, &values); + }); + } + #[test] fn query_iter_combinations() { let mut world = World::new(); @@ -253,47 +269,29 @@ mod tests { world.spawn(A(3)); world.spawn(A(4)); - let values: Vec<[&A; 2]> = world.query::<&A>().iter_combinations(&world).collect(); - assert_eq!( + let values: HashSet<[&A; 2]> = world.query::<&A>().iter_combinations(&world).collect(); + check_combinations( values, - vec![ + HashSet::from([ [&A(1), &A(2)], [&A(1), &A(3)], [&A(1), &A(4)], [&A(2), &A(3)], [&A(2), &A(4)], [&A(3), &A(4)], - ] + ]), ); let mut a_query = world.query::<&A>(); - let values: Vec<[&A; 3]> = a_query.iter_combinations(&world).collect(); - assert_eq!( + + let values: HashSet<[&A; 3]> = a_query.iter_combinations(&world).collect(); + check_combinations( values, - vec![ + HashSet::from([ [&A(1), &A(2), &A(3)], [&A(1), &A(2), &A(4)], [&A(1), &A(3), &A(4)], [&A(2), &A(3), &A(4)], - ] - ); - - let mut query = world.query::<&mut A>(); - let mut combinations = query.iter_combinations_mut(&mut world); - while let Some([mut a, mut b, mut c]) = combinations.fetch_next() { - a.0 += 10; - b.0 += 100; - c.0 += 1000; - } - - let values: Vec<[&A; 3]> = a_query.iter_combinations(&world).collect(); - assert_eq!( - values, - vec![ - [&A(31), &A(212), &A(1203)], - [&A(31), &A(212), &A(3004)], - [&A(31), &A(1203), &A(3004)], - [&A(212), &A(1203), &A(3004)] - ] + ]), ); let mut b_query = world.query::<&B>(); @@ -307,7 +305,7 @@ mod tests { #[test] fn query_filtered_iter_combinations() { - use bevy_ecs::query::{Added, Changed, Or, With, Without}; + use bevy_ecs::query::{Added, Or, With, Without}; let mut world = World::new(); @@ -318,33 +316,26 @@ mod tests { let mut a_wout_b = world.query_filtered::<&A, Without>(); let values: HashSet<[&A; 2]> = a_wout_b.iter_combinations(&world).collect(); - assert_eq!( + check_combinations( values, - [[&A(2), &A(3)], [&A(2), &A(4)], [&A(3), &A(4)]] - .into_iter() - .collect::>() + HashSet::from([[&A(2), &A(3)], [&A(2), &A(4)], [&A(3), &A(4)]]), ); let values: HashSet<[&A; 3]> = a_wout_b.iter_combinations(&world).collect(); - assert_eq!( - values, - [[&A(2), &A(3), &A(4)],].into_iter().collect::>() - ); + check_combinations(values, HashSet::from([[&A(2), &A(3), &A(4)]])); let mut query = world.query_filtered::<&A, Or<(With, With)>>(); let values: HashSet<[&A; 2]> = query.iter_combinations(&world).collect(); - assert_eq!( + check_combinations( values, - [ + HashSet::from([ [&A(1), &A(2)], [&A(1), &A(3)], [&A(1), &A(4)], [&A(2), &A(3)], [&A(2), &A(4)], [&A(3), &A(4)], - ] - .into_iter() - .collect::>() + ]), ); let mut query = world.query_filtered::<&mut A, Without>(); @@ -356,12 +347,7 @@ mod tests { } let values: HashSet<[&A; 3]> = a_wout_b.iter_combinations(&world).collect(); - assert_eq!( - values, - [[&A(12), &A(103), &A(1004)],] - .into_iter() - .collect::>() - ); + check_combinations(values, HashSet::from([[&A(12), &A(103), &A(1004)]])); // Check if Added, Changed works let mut world = World::new(); @@ -390,31 +376,6 @@ mod tests { world.spawn(A(10)); assert_eq!(query_added.iter_combinations::<2>(&world).count(), 3); - - world.clear_trackers(); - - let mut query_changed = world.query_filtered::<&A, Changed>(); - - let mut query = world.query_filtered::<&mut A, With>(); - let mut combinations = query.iter_combinations_mut(&mut world); - while let Some([mut a, mut b, mut c]) = combinations.fetch_next() { - a.0 += 10; - b.0 += 100; - c.0 += 1000; - } - - let values: HashSet<[&A; 3]> = query_changed.iter_combinations(&world).collect(); - assert_eq!( - values, - [ - [&A(31), &A(212), &A(1203)], - [&A(31), &A(212), &A(3004)], - [&A(31), &A(1203), &A(3004)], - [&A(212), &A(1203), &A(3004)] - ] - .into_iter() - .collect::>() - ); } #[test] @@ -423,24 +384,16 @@ mod tests { world.spawn_batch((1..=4).map(Sparse)); - let mut query = world.query::<&mut Sparse>(); - let mut combinations = query.iter_combinations_mut(&mut world); - while let Some([mut a, mut b, mut c]) = combinations.fetch_next() { - a.0 += 10; - b.0 += 100; - c.0 += 1000; - } - - let mut query = world.query::<&Sparse>(); - let values: Vec<[&Sparse; 3]> = query.iter_combinations(&world).collect(); - assert_eq!( + let values: HashSet<[&Sparse; 3]> = + world.query::<&Sparse>().iter_combinations(&world).collect(); + check_combinations( values, - vec![ - [&Sparse(31), &Sparse(212), &Sparse(1203)], - [&Sparse(31), &Sparse(212), &Sparse(3004)], - [&Sparse(31), &Sparse(1203), &Sparse(3004)], - [&Sparse(212), &Sparse(1203), &Sparse(3004)] - ] + HashSet::from([ + [&Sparse(1), &Sparse(2), &Sparse(3)], + [&Sparse(1), &Sparse(2), &Sparse(4)], + [&Sparse(1), &Sparse(3), &Sparse(4)], + [&Sparse(2), &Sparse(3), &Sparse(4)], + ]), ); } @@ -454,8 +407,9 @@ mod tests { let values = world .query::<&Sparse>() .iter(&world) - .collect::>(); - assert_eq!(values, vec![&Sparse(1), &Sparse(2)]); + .collect::>(); + assert!(values.contains(&Sparse(1))); + assert!(values.contains(&Sparse(2))); for (_a, mut b) in world.query::<(&Sparse, &mut B)>().iter_mut(&mut world) { b.0 = 3; @@ -491,13 +445,12 @@ mod tests { world.spawn((A(3), B(1))); world.spawn(A(4)); - let values: Vec<(&A, bool)> = world.query::<(&A, Has)>().iter(&world).collect(); + let values: HashSet<(&A, bool)> = world.query::<(&A, Has)>().iter(&world).collect(); - // The query seems to put the components with B first - assert_eq!( - values, - vec![(&A(1), true), (&A(3), true), (&A(2), false), (&A(4), false),] - ); + assert!(values.contains(&(&A(1), true))); + assert!(values.contains(&(&A(2), false))); + assert!(values.contains(&(&A(3), true))); + assert!(values.contains(&(&A(4), false))); } #[test] diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index dcb20250c2466f..70cf722daff3e4 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1,7 +1,7 @@ use crate::{ archetype::{Archetype, ArchetypeComponentId, ArchetypeGeneration, ArchetypeId}, batching::BatchingStrategy, - component::{ComponentId, Components, Tick}, + component::{ComponentId, Tick}, entity::Entity, prelude::FromWorld, query::{ @@ -341,16 +341,55 @@ impl QueryState { /// If `world` does not match the one used to call `QueryState::new` for this instance. pub fn update_archetypes_unsafe_world_cell(&mut self, world: UnsafeWorldCell) { self.validate_world(world.id()); - let archetypes = world.archetypes(); - let old_generation = - std::mem::replace(&mut self.archetype_generation, archetypes.generation()); - - for archetype in &archetypes[old_generation..] { - // SAFETY: The validate_world call ensures that the world is the same the QueryState - // was initialized from. - unsafe { - self.new_archetype_internal(archetype); + if self.component_access.required.is_empty() { + let archetypes = world.archetypes(); + let old_generation = + std::mem::replace(&mut self.archetype_generation, archetypes.generation()); + + for archetype in &archetypes[old_generation..] { + // SAFETY: The validate_world call ensures that the world is the same the QueryState + // was initialized from. + unsafe { + self.new_archetype_internal(archetype); + } + } + } else { + // skip if we are already up to date + if self.archetype_generation == world.archetypes().generation() { + return; + } + // if there are required components, we can optimize by only iterating through archetypes + // that contain at least one of the required components + let potential_archetypes = self + .component_access + .required + .ones() + .filter_map(|idx| { + let component_id = ComponentId::get_sparse_set_index(idx); + world + .archetypes() + .component_index() + .get(&component_id) + .map(|index| index.keys()) + }) + // select the component with the fewest archetypes + .min_by_key(ExactSizeIterator::len); + if let Some(archetypes) = potential_archetypes { + for archetype_id in archetypes { + // exclude archetypes that have already been processed + if archetype_id < &self.archetype_generation.0 { + continue; + } + // SAFETY: get_potential_archetypes only returns archetype ids that are valid for the world + let archetype = &world.archetypes()[*archetype_id]; + // SAFETY: The validate_world call ensures that the world is the same the QueryState + // was initialized from. + unsafe { + self.new_archetype_internal(archetype); + } + } } + self.archetype_generation = world.archetypes().generation(); } } @@ -456,16 +495,22 @@ impl QueryState { archetype: &Archetype, access: &mut Access, ) { - self.component_access.access.reads().for_each(|id| { - if let Some(id) = archetype.get_archetype_component_id(id) { - access.add_read(id); - } - }); - self.component_access.access.writes().for_each(|id| { - if let Some(id) = archetype.get_archetype_component_id(id) { - access.add_write(id); - } - }); + self.component_access + .access + .component_reads() + .for_each(|id| { + if let Some(id) = archetype.get_archetype_component_id(id) { + access.add_component_read(id); + } + }); + self.component_access + .access + .component_writes() + .for_each(|id| { + if let Some(id) = archetype.get_archetype_component_id(id) { + access.add_component_write(id); + } + }); } /// Use this to transform a [`QueryState`] into a more generic [`QueryState`]. @@ -476,21 +521,27 @@ impl QueryState { /// 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, components: &Components) -> QueryState { - self.transmute_filtered::(components) + pub fn transmute<'a, NewD: QueryData>( + &self, + world: impl Into>, + ) -> QueryState { + self.transmute_filtered::(world.into()) } /// 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( + pub fn transmute_filtered<'a, NewD: QueryData, NewF: QueryFilter>( &self, - components: &Components, + world: impl Into>, ) -> QueryState { + let world = world.into(); + self.validate_world(world.id()); + let mut component_access = FilteredAccess::default(); - let mut fetch_state = NewD::get_state(components).expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); - let filter_state = NewF::get_state(components).expect("Could not create filter_state, Please initialize all referenced components before transmuting."); + let mut fetch_state = NewD::get_state(world.components()).expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); + let filter_state = NewF::get_state(world.components()).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); @@ -542,12 +593,12 @@ impl QueryState { /// ## Panics /// /// Will panic if `NewD` contains accesses not in `Q` or `OtherQ`. - pub fn join( + pub fn join<'a, OtherD: QueryData, NewD: QueryData>( &self, - components: &Components, + world: impl Into>, other: &QueryState, ) -> QueryState { - self.join_filtered::<_, (), NewD, ()>(components, other) + self.join_filtered::<_, (), NewD, ()>(world, other) } /// Use this to combine two queries. The data accessed will be the intersection @@ -557,23 +608,28 @@ impl QueryState { /// /// Will panic if `NewD` or `NewF` requires accesses not in `Q` or `OtherQ`. pub fn join_filtered< + 'a, OtherD: QueryData, OtherF: QueryFilter, NewD: QueryData, NewF: QueryFilter, >( &self, - components: &Components, + world: impl Into>, other: &QueryState, ) -> QueryState { if self.world_id != other.world_id { panic!("Joining queries initialized on different worlds is not allowed."); } + let world = world.into(); + + self.validate_world(world.id()); + let mut component_access = FilteredAccess::default(); - let mut new_fetch_state = NewD::get_state(components) + let mut new_fetch_state = NewD::get_state(world.components()) .expect("Could not create fetch_state, Please initialize all referenced components before transmuting."); - let new_filter_state = NewF::get_state(components) + let new_filter_state = NewF::get_state(world.components()) .expect("Could not create filter_state, Please initialize all referenced components before transmuting."); NewD::set_access(&mut new_fetch_state, &self.component_access); @@ -1529,7 +1585,7 @@ impl QueryState { pub fn single<'w>(&mut self, world: &'w World) -> ROQueryItem<'w, D> { match self.get_single(world) { Ok(items) => items, - Err(error) => panic!("Cannot get single mutable query result: {error}"), + Err(error) => panic!("Cannot get single query result: {error}"), } } @@ -1779,7 +1835,7 @@ mod tests { world.spawn((A(1), B(0))); let query_state = world.query::<(&A, &B)>(); - let mut new_query_state = query_state.transmute::<&A>(world.components()); + 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); @@ -1793,7 +1849,7 @@ mod tests { 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.components()); + 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); @@ -1807,7 +1863,7 @@ mod tests { let entity = world.spawn(A(10)).id(); let q = world.query::<()>(); - let mut q = q.transmute::(world.components()); + let mut q = q.transmute::(&world); assert_eq!(q.single(&world), entity); } @@ -1817,11 +1873,11 @@ mod tests { world.spawn(A(10)); let q = world.query::<&A>(); - let mut new_q = q.transmute::>(world.components()); + let mut new_q = q.transmute::>(&world); assert!(new_q.single(&world).is_added()); let q = world.query::>(); - let _ = q.transmute::<&A>(world.components()); + let _ = q.transmute::<&A>(&world); } #[test] @@ -1830,8 +1886,8 @@ mod tests { world.spawn(A(0)); let q = world.query::<&mut A>(); - let _ = q.transmute::>(world.components()); - let _ = q.transmute::<&A>(world.components()); + let _ = q.transmute::>(&world); + let _ = q.transmute::<&A>(&world); } #[test] @@ -1840,7 +1896,7 @@ mod tests { world.spawn(A(0)); let q: QueryState> = world.query::(); - let _ = q.transmute::(world.components()); + let _ = q.transmute::(&world); } #[test] @@ -1849,8 +1905,8 @@ mod tests { world.spawn((A(0), B(0))); let query_state = world.query::<(Option<&A>, &B)>(); - let _ = query_state.transmute::>(world.components()); - let _ = query_state.transmute::<&B>(world.components()); + let _ = query_state.transmute::>(&world); + let _ = query_state.transmute::<&B>(&world); } #[test] @@ -1864,7 +1920,7 @@ mod tests { world.spawn(A(0)); let query_state = world.query::<&A>(); - let mut _new_query_state = query_state.transmute::<(&A, &B)>(world.components()); + let mut _new_query_state = query_state.transmute::<(&A, &B)>(&world); } #[test] @@ -1876,7 +1932,7 @@ mod tests { world.spawn(A(0)); let query_state = world.query::<&A>(); - let mut _new_query_state = query_state.transmute::<&mut A>(world.components()); + let mut _new_query_state = query_state.transmute::<&mut A>(&world); } #[test] @@ -1888,7 +1944,7 @@ mod tests { world.spawn(C(0)); let query_state = world.query::>(); - let mut new_query_state = query_state.transmute::<&A>(world.components()); + let mut new_query_state = query_state.transmute::<&A>(&world); let x = new_query_state.single(&world); assert_eq!(x.0, 1234); } @@ -1902,15 +1958,15 @@ mod tests { world.init_component::(); let q = world.query::(); - let _ = q.transmute::<&A>(world.components()); + 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.components()); + let query = + QueryState::<(Entity, &A, &B)>::new(&mut world).transmute::(&world); let mut query = query; // Our result is completely untyped @@ -1927,7 +1983,7 @@ mod tests { let entity_a = world.spawn(A(0)).id(); let mut query = QueryState::<(Entity, &A, Has)>::new(&mut world) - .transmute_filtered::<(Entity, Has), Added>(world.components()); + .transmute_filtered::<(Entity, Has), Added>(&world); assert_eq!((entity_a, false), query.single(&world)); @@ -1947,7 +2003,7 @@ mod tests { let entity_a = world.spawn(A(0)).id(); let mut detection_query = QueryState::<(Entity, &A)>::new(&mut world) - .transmute_filtered::>(world.components()); + .transmute_filtered::>(&world); let mut change_query = QueryState::<&mut A>::new(&mut world); assert_eq!(entity_a, detection_query.single(&world)); @@ -1970,7 +2026,20 @@ mod tests { world.init_component::(); world.init_component::(); let query = QueryState::<&A>::new(&mut world); - let _new_query = query.transmute_filtered::>(world.components()); + let _new_query = query.transmute_filtered::>(&world); + } + + // Regression test for #14629 + #[test] + #[should_panic] + fn transmute_with_different_world() { + let mut world = World::new(); + world.spawn((A(1), B(2))); + + let mut world2 = World::new(); + world2.init_component::(); + + world.query::<(&A, &B)>().transmute::<&B>(&world2); } #[test] @@ -1983,8 +2052,7 @@ mod tests { let query_1 = QueryState::<&A, Without>::new(&mut world); let query_2 = QueryState::<&B, Without>::new(&mut world); - let mut new_query: QueryState = - query_1.join_filtered(world.components(), &query_2); + let mut new_query: QueryState = query_1.join_filtered(&world, &query_2); assert_eq!(new_query.single(&world), entity_ab); } @@ -1999,8 +2067,7 @@ mod tests { let query_1 = QueryState::<&A>::new(&mut world); let query_2 = QueryState::<&B, Without>::new(&mut world); - let mut new_query: QueryState = - query_1.join_filtered(world.components(), &query_2); + let mut new_query: QueryState = query_1.join_filtered(&world, &query_2); assert!(new_query.get(&world, entity_ab).is_ok()); // should not be able to get entity with c. @@ -2016,7 +2083,7 @@ mod tests { world.init_component::(); let query_1 = QueryState::<&A>::new(&mut world); let query_2 = QueryState::<&B>::new(&mut world); - let _query: QueryState<&C> = query_1.join(world.components(), &query_2); + let _query: QueryState<&C> = query_1.join(&world, &query_2); } #[test] @@ -2030,6 +2097,6 @@ mod tests { let mut world = World::new(); let query_1 = QueryState::<&A, Without>::new(&mut world); let query_2 = QueryState::<&B, Without>::new(&mut world); - let _: QueryState> = query_1.join_filtered(world.components(), &query_2); + let _: QueryState> = query_1.join_filtered(&world, &query_2); } } diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index 7c8283cbfe196e..f2da8b3558f26e 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -53,6 +53,9 @@ pub unsafe trait WorldQuery { /// This function manually implements subtyping for the query items. fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort>; + /// This function manually implements subtyping for the query fetches. + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort>; + /// Creates a new instance of this fetch. /// /// # Safety @@ -144,10 +147,11 @@ pub unsafe trait WorldQuery { } macro_rules! impl_tuple_world_query { - ($(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { #[allow(non_snake_case)] #[allow(clippy::unused_unit)] + $(#[$meta])* /// SAFETY: /// `fetch` accesses are the conjunction of the subqueries' accesses /// This is sound because `update_component_access` adds accesses according to the implementations of all the subqueries. @@ -165,6 +169,13 @@ macro_rules! impl_tuple_world_query { )*) } + fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + let ($($name,)*) = fetch; + ($( + $name::shrink_fetch($name), + )*) + } + #[inline] #[allow(clippy::unused_unit)] unsafe fn init_fetch<'w>(_world: UnsafeWorldCell<'w>, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { @@ -229,4 +240,11 @@ macro_rules! impl_tuple_world_query { }; } -all_tuples!(impl_tuple_world_query, 0, 15, F, S); +all_tuples!( + #[doc(fake_variadic)] + impl_tuple_world_query, + 0, + 15, + F, + S +); diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index 7f905015458d5c..b594506c7a824a 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -4,13 +4,15 @@ //! This module exports two types: [`ReflectBundleFns`] and [`ReflectBundle`]. //! //! Same as [`super::component`], but for bundles. -use std::any::TypeId; +use std::any::{Any, TypeId}; use crate::{ prelude::Bundle, world::{EntityMut, EntityWorldMut}, }; -use bevy_reflect::{FromReflect, FromType, Reflect, ReflectRef, TypeRegistry}; +use bevy_reflect::{ + FromReflect, FromType, PartialReflect, Reflect, ReflectRef, TypePath, TypeRegistry, +}; use super::{from_reflect_with_fallback, ReflectComponent}; @@ -27,11 +29,11 @@ pub struct ReflectBundle(ReflectBundleFns); #[derive(Clone)] pub struct ReflectBundleFns { /// Function pointer implementing [`ReflectBundle::insert()`]. - pub insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), + pub insert: fn(&mut EntityWorldMut, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectBundle::apply()`]. - pub apply: fn(EntityMut, &dyn Reflect, &TypeRegistry), + pub apply: fn(EntityMut, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectBundle::apply_or_insert()`]. - pub apply_or_insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), + pub apply_or_insert: fn(&mut EntityWorldMut, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectBundle::remove()`]. pub remove: fn(&mut EntityWorldMut), } @@ -42,7 +44,7 @@ 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 } } @@ -52,7 +54,7 @@ impl ReflectBundle { pub fn insert( &self, entity: &mut EntityWorldMut, - bundle: &dyn Reflect, + bundle: &dyn PartialReflect, registry: &TypeRegistry, ) { (self.0.insert)(entity, bundle, registry); @@ -66,7 +68,7 @@ impl ReflectBundle { pub fn apply<'a>( &self, entity: impl Into>, - bundle: &dyn Reflect, + bundle: &dyn PartialReflect, registry: &TypeRegistry, ) { (self.0.apply)(entity.into(), bundle, registry); @@ -76,7 +78,7 @@ impl ReflectBundle { pub fn apply_or_insert( &self, entity: &mut EntityWorldMut, - bundle: &dyn Reflect, + bundle: &dyn PartialReflect, registry: &TypeRegistry, ) { (self.0.apply_or_insert)(entity, bundle, registry); @@ -122,7 +124,7 @@ impl ReflectBundle { } } -impl FromType for ReflectBundle { +impl FromType for ReflectBundle { fn from_type() -> Self { ReflectBundle(ReflectBundleFns { insert: |entity, reflected_bundle, registry| { @@ -180,10 +182,16 @@ impl FromType for ReflectBundle { } } -fn apply_field(entity: &mut EntityMut, field: &dyn Reflect, registry: &TypeRegistry) { - if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { +fn apply_field(entity: &mut EntityMut, field: &dyn PartialReflect, registry: &TypeRegistry) { + let Some(type_id) = field.try_as_reflect().map(Any::type_id) else { + panic!( + "`{}` did not implement `Reflect`", + field.reflect_type_path() + ); + }; + if let Some(reflect_component) = registry.get_type_data::(type_id) { reflect_component.apply(entity.reborrow(), field); - } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { + } else if let Some(reflect_bundle) = registry.get_type_data::(type_id) { reflect_bundle.apply(entity.reborrow(), field, registry); } else { panic!( @@ -195,19 +203,22 @@ fn apply_field(entity: &mut EntityMut, field: &dyn Reflect, registry: &TypeRegis fn apply_or_insert_field( entity: &mut EntityWorldMut, - field: &dyn Reflect, + field: &dyn PartialReflect, registry: &TypeRegistry, ) { - if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { + let Some(type_id) = field.try_as_reflect().map(Any::type_id) else { + panic!( + "`{}` did not implement `Reflect`", + field.reflect_type_path() + ); + }; + + if let Some(reflect_component) = registry.get_type_data::(type_id) { reflect_component.apply_or_insert(entity, field, registry); - } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { + } else if let Some(reflect_bundle) = registry.get_type_data::(type_id) { reflect_bundle.apply_or_insert(entity, field, registry); } else { - let is_component = entity - .world() - .components() - .get_id(field.type_id()) - .is_some(); + let is_component = entity.world().components().get_id(type_id).is_some(); if is_component { panic!( diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index f211597e41ec1a..b3422208d1f4b9 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -67,7 +67,7 @@ use crate::{ FilteredEntityRef, World, }, }; -use bevy_reflect::{FromReflect, FromType, Reflect, TypeRegistry}; +use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry}; /// A struct used to operate on reflected [`Component`] trait of a type. /// @@ -99,11 +99,11 @@ pub struct ReflectComponent(ReflectComponentFns); #[derive(Clone)] pub struct ReflectComponentFns { /// Function pointer implementing [`ReflectComponent::insert()`]. - pub insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), + pub insert: fn(&mut EntityWorldMut, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectComponent::apply()`]. - pub apply: fn(EntityMut, &dyn Reflect), + pub apply: fn(EntityMut, &dyn PartialReflect), /// Function pointer implementing [`ReflectComponent::apply_or_insert()`]. - pub apply_or_insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), + pub apply_or_insert: fn(&mut EntityWorldMut, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectComponent::remove()`]. pub remove: fn(&mut EntityWorldMut), /// Function pointer implementing [`ReflectComponent::contains()`]. @@ -127,7 +127,7 @@ 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 } } @@ -137,7 +137,7 @@ impl ReflectComponent { pub fn insert( &self, entity: &mut EntityWorldMut, - component: &dyn Reflect, + component: &dyn PartialReflect, registry: &TypeRegistry, ) { (self.0.insert)(entity, component, registry); @@ -148,7 +148,7 @@ impl ReflectComponent { /// # Panics /// /// Panics if there is no [`Component`] of the given type. - pub fn apply<'a>(&self, entity: impl Into>, component: &dyn Reflect) { + pub fn apply<'a>(&self, entity: impl Into>, component: &dyn PartialReflect) { (self.0.apply)(entity.into(), component); } @@ -156,7 +156,7 @@ impl ReflectComponent { pub fn apply_or_insert( &self, entity: &mut EntityWorldMut, - component: &dyn Reflect, + component: &dyn PartialReflect, registry: &TypeRegistry, ) { (self.0.apply_or_insert)(entity, component, registry); @@ -256,7 +256,7 @@ impl ReflectComponent { } } -impl FromType for ReflectComponent { +impl FromType for ReflectComponent { fn from_type() -> Self { ReflectComponent(ReflectComponentFns { insert: |entity, reflected_component, registry| { @@ -271,7 +271,7 @@ impl FromType for ReflectComponent { }, apply_or_insert: |entity, reflected_component, registry| { if let Some(mut component) = entity.get_mut::() { - component.apply(reflected_component); + component.apply(reflected_component.as_partial_reflect()); } else { let component = entity.world_scope(|world| { from_reflect_with_fallback::(reflected_component, world, registry) @@ -293,20 +293,15 @@ impl FromType for ReflectComponent { }, reflect: |entity| entity.get::().map(|c| c as &dyn Reflect), reflect_mut: |entity| { - entity.into_mut::().map(|c| Mut { - value: c.value as &mut dyn Reflect, - ticks: c.ticks, - }) + entity + .into_mut::() + .map(|c| c.map_unchanged(|value| value as &mut dyn Reflect)) }, reflect_unchecked_mut: |entity| { // SAFETY: reflect_unchecked_mut is an unsafe function pointer used by // `reflect_unchecked_mut` which must be called with an UnsafeEntityCell with access to the component `C` on the `entity` - unsafe { - entity.get_mut::().map(|c| Mut { - value: c.value as &mut dyn Reflect, - ticks: c.ticks, - }) - } + let c = unsafe { entity.get_mut::() }; + c.map(|c| c.map_unchanged(|value| value as &mut dyn Reflect)) }, }) } diff --git a/crates/bevy_ecs/src/reflect/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs index 1a3a0138fe8967..a48d1299bd3a6f 100644 --- a/crates/bevy_ecs/src/reflect/entity_commands.rs +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -3,7 +3,7 @@ use crate::reflect::AppTypeRegistry; use crate::system::{EntityCommands, Resource}; use crate::world::Command; use crate::{entity::Entity, reflect::ReflectComponent, world::World}; -use bevy_reflect::{Reflect, TypeRegistry}; +use bevy_reflect::{PartialReflect, TypeRegistry}; use std::borrow::Cow; use std::marker::PhantomData; @@ -18,7 +18,7 @@ pub trait ReflectCommandExt { /// /// - If the entity doesn't exist. /// - If [`AppTypeRegistry`] does not have the reflection data for the given [`Component`](crate::component::Component). - /// - If the component data is invalid. See [`Reflect::apply`] for further details. + /// - If the component data is invalid. See [`PartialReflect::apply`] for further details. /// - If [`AppTypeRegistry`] is not present in the [`World`]. /// /// # Note @@ -69,7 +69,7 @@ pub trait ReflectCommandExt { /// } /// /// ``` - fn insert_reflect(&mut self, component: Box) -> &mut Self; + fn insert_reflect(&mut self, component: Box) -> &mut Self; /// Same as [`insert_reflect`](ReflectCommandExt::insert_reflect), but using the `T` resource as type registry instead of /// `AppTypeRegistry`. @@ -83,7 +83,7 @@ pub trait ReflectCommandExt { /// - The given [`Resource`] is removed from the [`World`] before the command is applied. fn insert_reflect_with_registry>( &mut self, - component: Box, + component: Box, ) -> &mut Self; /// Removes from the entity the component with the given type name registered in [`AppTypeRegistry`]. @@ -142,7 +142,7 @@ pub trait ReflectCommandExt { } impl ReflectCommandExt for EntityCommands<'_> { - fn insert_reflect(&mut self, component: Box) -> &mut Self { + fn insert_reflect(&mut self, component: Box) -> &mut Self { self.commands.add(InsertReflect { entity: self.entity, component, @@ -152,7 +152,7 @@ impl ReflectCommandExt for EntityCommands<'_> { fn insert_reflect_with_registry>( &mut self, - component: Box, + component: Box, ) -> &mut Self { self.commands.add(InsertReflectWithRegistry:: { entity: self.entity, @@ -188,7 +188,7 @@ fn insert_reflect( world: &mut World, entity: Entity, type_registry: &TypeRegistry, - component: Box, + component: Box, ) { let type_info = component .get_represented_type_info() @@ -197,13 +197,13 @@ fn insert_reflect( let Some(mut entity) = world.get_entity_mut(entity) else { panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003"); }; - let Some(type_registration) = type_registry.get_with_type_path(type_path) else { + let Some(type_registration) = type_registry.get(type_info.type_id()) else { panic!("Could not get type registration (for component type {type_path}) because it doesn't exist in the TypeRegistry."); }; 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, type_registry); + reflect_component.insert(&mut entity, component.as_partial_reflect(), type_registry); } /// A [`Command`] that adds the boxed reflect component to an entity using the data in @@ -214,7 +214,7 @@ pub struct InsertReflect { /// The entity on which the component will be inserted. pub entity: Entity, /// The reflect [`Component`](crate::component::Component) that will be added to the entity. - pub component: Box, + pub component: Box, } impl Command for InsertReflect { @@ -233,7 +233,7 @@ pub struct InsertReflectWithRegistry> { pub entity: Entity, pub _t: PhantomData, /// The reflect [`Component`](crate::component::Component) that will be added to the entity. - pub component: Box, + pub component: Box, } impl> Command for InsertReflectWithRegistry { @@ -317,7 +317,7 @@ mod tests { use crate::system::{Commands, SystemState}; use crate::{self as bevy_ecs, component::Component, world::World}; use bevy_ecs_macros::Resource; - use bevy_reflect::{Reflect, TypeRegistry}; + use bevy_reflect::{PartialReflect, Reflect, TypeRegistry}; #[derive(Resource)] struct TypeRegistryResource { @@ -352,7 +352,7 @@ mod tests { let entity = commands.spawn_empty().id(); let entity2 = commands.spawn_empty().id(); - let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; + let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; let boxed_reflect_component_a_clone = boxed_reflect_component_a.clone_value(); commands @@ -388,7 +388,7 @@ mod tests { let entity = commands.spawn_empty().id(); - let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; + let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; commands .entity(entity) diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index 322bf3c9b544c5..ae02af891314fd 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -6,7 +6,9 @@ use std::ops::{Deref, DerefMut}; use crate as bevy_ecs; use crate::{system::Resource, world::World}; use bevy_reflect::std_traits::ReflectDefault; -use bevy_reflect::{Reflect, ReflectFromReflect, TypeRegistry, TypeRegistryArc}; +use bevy_reflect::{ + PartialReflect, Reflect, ReflectFromReflect, TypePath, TypeRegistry, TypeRegistryArc, +}; mod bundle; mod component; @@ -43,7 +45,33 @@ impl DerefMut for AppTypeRegistry { } } -/// Creates a `T` from a `&dyn Reflect`. +/// A [`Resource`] storing [`FunctionRegistry`] for +/// function registrations relevant to a whole app. +/// +/// [`FunctionRegistry`]: bevy_reflect::func::FunctionRegistry +#[cfg(feature = "reflect_functions")] +#[derive(Resource, Clone, Default)] +pub struct AppFunctionRegistry(pub bevy_reflect::func::FunctionRegistryArc); + +#[cfg(feature = "reflect_functions")] +impl Deref for AppFunctionRegistry { + type Target = bevy_reflect::func::FunctionRegistryArc; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "reflect_functions")] +impl DerefMut for AppFunctionRegistry { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Creates a `T` from a `&dyn PartialReflect`. /// /// This will try the following strategies, in this order: /// @@ -59,18 +87,17 @@ impl DerefMut for AppTypeRegistry { /// this method will panic. /// /// If none of the strategies succeed, this method will panic. -fn from_reflect_with_fallback( - reflected: &dyn Reflect, +pub fn from_reflect_with_fallback( + reflected: &dyn PartialReflect, world: &mut World, registry: &TypeRegistry, ) -> T { - fn different_type_error(reflected: &str) -> ! { + fn different_type_error(reflected: &str) -> ! { panic!( "The registration for the reflected `{}` trait for the type `{}` produced \ a value of a different type", reflected, - // FIXME: once we have unique reflect, use `TypePath`. - std::any::type_name::(), + T::type_path(), ); } diff --git a/crates/bevy_ecs/src/reflect/resource.rs b/crates/bevy_ecs/src/reflect/resource.rs index a537e3b55ded0e..e55ffb964a97c2 100644 --- a/crates/bevy_ecs/src/reflect/resource.rs +++ b/crates/bevy_ecs/src/reflect/resource.rs @@ -9,7 +9,7 @@ use crate::{ system::Resource, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; -use bevy_reflect::{FromReflect, FromType, Reflect, TypeRegistry}; +use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry}; use super::from_reflect_with_fallback; @@ -43,11 +43,11 @@ pub struct ReflectResource(ReflectResourceFns); #[derive(Clone)] pub struct ReflectResourceFns { /// Function pointer implementing [`ReflectResource::insert()`]. - pub insert: fn(&mut World, &dyn Reflect, &TypeRegistry), + pub insert: fn(&mut World, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectResource::apply()`]. - pub apply: fn(&mut World, &dyn Reflect), + pub apply: fn(&mut World, &dyn PartialReflect), /// Function pointer implementing [`ReflectResource::apply_or_insert()`]. - pub apply_or_insert: fn(&mut World, &dyn Reflect, &TypeRegistry), + pub apply_or_insert: fn(&mut World, &dyn PartialReflect, &TypeRegistry), /// Function pointer implementing [`ReflectResource::remove()`]. pub remove: fn(&mut World), /// Function pointer implementing [`ReflectResource::reflect()`]. @@ -67,14 +67,19 @@ impl ReflectResourceFns { /// /// 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 ReflectResource { /// Insert a reflected [`Resource`] into the world like [`insert()`](World::insert_resource). - pub fn insert(&self, world: &mut World, resource: &dyn Reflect, registry: &TypeRegistry) { + pub fn insert( + &self, + world: &mut World, + resource: &dyn PartialReflect, + registry: &TypeRegistry, + ) { (self.0.insert)(world, resource, registry); } @@ -83,7 +88,7 @@ impl ReflectResource { /// # Panics /// /// Panics if there is no [`Resource`] of the given type. - pub fn apply(&self, world: &mut World, resource: &dyn Reflect) { + pub fn apply(&self, world: &mut World, resource: &dyn PartialReflect) { (self.0.apply)(world, resource); } @@ -91,7 +96,7 @@ impl ReflectResource { pub fn apply_or_insert( &self, world: &mut World, - resource: &dyn Reflect, + resource: &dyn PartialReflect, registry: &TypeRegistry, ) { (self.0.apply_or_insert)(world, resource, registry); @@ -176,7 +181,7 @@ impl ReflectResource { } } -impl FromType for ReflectResource { +impl FromType for ReflectResource { fn from_type() -> Self { ReflectResource(ReflectResourceFns { insert: |world, reflected_resource, registry| { @@ -203,12 +208,8 @@ impl FromType for ReflectResource { reflect_unchecked_mut: |world| { // SAFETY: all usages of `reflect_unchecked_mut` guarantee that there is either a single mutable // reference or multiple immutable ones alive at any given point - unsafe { - world.get_resource_mut::().map(|res| Mut { - value: res.value as &mut dyn Reflect, - ticks: res.ticks, - }) - } + let res = unsafe { world.get_resource_mut::() }; + res.map(|res| res.map_unchanged(|value| value as &mut dyn Reflect)) }, copy: |source_world, destination_world, registry| { let source_resource = source_world.resource::(); diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 4259b6d1f2fb57..fb030467a8a42d 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -496,11 +496,11 @@ pub mod common_conditions { event::{Event, EventReader}, prelude::{Component, Query, With}, removal_detection::RemovedComponents, - system::{IntoSystem, Res, Resource, System}, + system::{IntoSystem, Local, Res, Resource, System}, }; - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` - /// if the first time the condition is run and false every time after + /// A [`Condition`](super::Condition)-satisfying system that returns `true` + /// on the first time the condition is run and false every time after. /// /// # Example /// @@ -513,7 +513,7 @@ pub mod common_conditions { /// # world.init_resource::(); /// app.add_systems( /// // `run_once` will only return true the first time it's evaluated - /// my_system.run_if(run_once()), + /// my_system.run_if(run_once), /// ); /// /// fn my_system(mut counter: ResMut) { @@ -528,15 +528,12 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn run_once() -> impl FnMut() -> bool + Clone { - let mut has_run = false; - move || { - if !has_run { - has_run = true; - true - } else { - false - } + pub fn run_once(mut has_run: Local) -> bool { + if !*has_run { + *has_run = true; + true + } else { + false } } @@ -815,7 +812,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 had its value changed since the condition /// was last checked. /// @@ -841,7 +838,7 @@ pub mod common_conditions { /// // `resource_changed_or_removed` will only return true if the /// // given resource was just changed or removed (or added) /// my_system.run_if( - /// resource_changed_or_removed::() + /// resource_changed_or_removed:: /// // 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 @@ -877,25 +874,22 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.contains_resource::(), true); /// ``` - pub fn resource_changed_or_removed() -> impl FnMut(Option>) -> bool + Clone + pub fn resource_changed_or_removed(res: Option>, mut existed: Local) -> bool where T: Resource, { - let mut existed = false; - move |res: Option>| { - if let Some(value) = res { - existed = true; - value.is_changed() - } else if existed { - existed = false; - true - } else { - false - } + if let Some(value) = res { + *existed = true; + value.is_changed() + } else if *existed { + *existed = false; + true + } else { + 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 been removed since the condition was last checked. /// /// # Example @@ -910,7 +904,7 @@ pub mod common_conditions { /// app.add_systems( /// // `resource_removed` will only return true if the /// // given resource was just removed - /// my_system.run_if(resource_removed::()), + /// my_system.run_if(resource_removed::), /// ); /// /// #[derive(Resource, Default)] @@ -932,25 +926,22 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn resource_removed() -> impl FnMut(Option>) -> bool + Clone + pub fn resource_removed(res: Option>, mut existed: Local) -> bool where T: Resource, { - let mut existed = false; - move |res: Option>| { - if res.is_some() { - existed = true; - false - } else if existed { - existed = false; - true - } else { - false - } + if res.is_some() { + *existed = true; + false + } else if *existed { + *existed = false; + true + } else { + false } } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if there are any new events of the given type since it was last called. /// /// # Example @@ -966,7 +957,7 @@ pub mod common_conditions { /// # app.add_systems(bevy_ecs::event::event_update_system.before(my_system)); /// /// app.add_systems( - /// my_system.run_if(on_event::()), + /// my_system.run_if(on_event::), /// ); /// /// #[derive(Event)] @@ -986,12 +977,12 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn on_event() -> impl FnMut(EventReader) -> bool + Clone { + pub fn on_event(mut reader: EventReader) -> bool { // The events need to be consumed, so that there are no false positives on subsequent // calls of the run condition. Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), // due to Bevy having a specialized implementation for events. - move |mut reader: EventReader| reader.read().count() > 0 + reader.read().count() > 0 } /// A [`Condition`](super::Condition)-satisfying system that returns `true` @@ -1031,15 +1022,15 @@ pub mod common_conditions { !query.is_empty() } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// A [`Condition`](super::Condition)-satisfying system that returns `true` /// if there are any entity with a component of the given type removed. - pub fn any_component_removed() -> impl FnMut(RemovedComponents) -> bool { + pub fn any_component_removed(mut removals: RemovedComponents) -> bool { // `RemovedComponents` based on events and therefore events need to be consumed, // so that there are no false positives on subsequent calls of the run condition. // Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), // due to Bevy having a specialized implementation for events. - move |mut removals: RemovedComponents| removals.read().count() != 0 + removals.read().count() > 0 } /// Generates a [`Condition`](super::Condition) that inverses the result of passed one. @@ -1384,16 +1375,16 @@ mod tests { fn distributive_run_if_compiles() { Schedule::default().add_systems( (test_system, test_system) - .distributive_run_if(run_once()) + .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_changed_or_removed::()) - .distributive_run_if(resource_removed::()) - .distributive_run_if(on_event::()) + .distributive_run_if(resource_changed_or_removed::) + .distributive_run_if(resource_removed::) + .distributive_run_if(on_event::) .distributive_run_if(any_with_component::) - .distributive_run_if(not(run_once())), + .distributive_run_if(not(run_once)), ); } } diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index ddf725260141b0..55f38ca19c3ea6 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -521,7 +521,8 @@ impl IntoSystemConfigs<()> for SystemConfigs { pub struct SystemConfigTupleMarker; macro_rules! impl_system_collection { - ($(($param: ident, $sys: ident)),*) => { + ($(#[$meta:meta])* $(($param: ident, $sys: ident)),*) => { + $(#[$meta])* impl<$($param, $sys),*> IntoSystemConfigs<(SystemConfigTupleMarker, $($param,)*)> for ($($sys,)*) where $($sys: IntoSystemConfigs<$param>),* @@ -539,7 +540,14 @@ macro_rules! impl_system_collection { } } -all_tuples!(impl_system_collection, 1, 20, P, S); +all_tuples!( + #[doc(fake_variadic)] + impl_system_collection, + 1, + 20, + P, + S +); /// A [`SystemSet`] with scheduling metadata. pub type SystemSetConfig = NodeConfig; @@ -740,7 +748,8 @@ impl IntoSystemSetConfigs for SystemSetConfig { } macro_rules! impl_system_set_collection { - ($($set: ident),*) => { + ($(#[$meta:meta])* $($set: ident),*) => { + $(#[$meta])* impl<$($set: IntoSystemSetConfigs),*> IntoSystemSetConfigs for ($($set,)*) { #[allow(non_snake_case)] @@ -756,4 +765,10 @@ macro_rules! impl_system_set_collection { } } -all_tuples!(impl_system_set_collection, 1, 20, S); +all_tuples!( + #[doc(fake_variadic)] + impl_system_set_collection, + 1, + 20, + S +); diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 3d76fdb1842317..10197af3e4f777 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -30,7 +30,7 @@ pub(super) trait SystemExecutor: Send + Sync { /// Specifies how a [`Schedule`](super::Schedule) will be run. /// /// The default depends on the target platform: -/// - [`SingleThreaded`](ExecutorKind::SingleThreaded) on WASM. +/// - [`SingleThreaded`](ExecutorKind::SingleThreaded) on Wasm. /// - [`MultiThreaded`](ExecutorKind::MultiThreaded) everywhere else. #[derive(PartialEq, Eq, Default, Debug, Copy, Clone)] pub enum ExecutorKind { diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 184bef250a6b55..28aa65c59d2b99 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -737,6 +737,9 @@ mod tests { #[derive(Event)] struct E; + #[derive(Resource, Component)] + struct RC; + fn empty_system() {} fn res_system(_res: Res) {} fn resmut_system(_res: ResMut) {} @@ -746,6 +749,8 @@ mod tests { fn write_component_system(_query: Query<&mut A>) {} fn with_filtered_component_system(_query: Query<&mut A, With>) {} fn without_filtered_component_system(_query: Query<&mut A, Without>) {} + fn entity_ref_system(_query: Query) {} + fn entity_mut_system(_query: Query) {} fn event_reader_system(_reader: EventReader) {} fn event_writer_system(_writer: EventWriter) {} fn event_resource_system(_events: ResMut>) {} @@ -788,6 +793,8 @@ mod tests { nonsend_system, read_component_system, read_component_system, + entity_ref_system, + entity_ref_system, event_reader_system, event_reader_system, read_world_system, @@ -893,6 +900,73 @@ mod tests { assert_eq!(schedule.graph().conflicting_systems().len(), 3); } + /// Test that when a struct is both a Resource and a Component, they do not + /// conflict with each other. + #[test] + fn shared_resource_mut_component() { + let mut world = World::new(); + world.insert_resource(RC); + + let mut schedule = Schedule::default(); + schedule.add_systems((|_: ResMut| {}, |_: Query<&mut RC>| {})); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn resource_mut_and_entity_ref() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems((resmut_system, entity_ref_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn resource_and_entity_mut() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems((res_system, nonsend_system, entity_mut_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn write_component_and_entity_ref() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems((write_component_system, entity_ref_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 1); + } + + #[test] + fn read_component_and_entity_mut() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems((read_component_system, entity_mut_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 1); + } + #[test] fn exclusive() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index c9dc06438f0cfa..dad5d99c56f9ef 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -23,6 +23,8 @@ use crate::{ world::World, }; +use crate::query::AccessConflicts; +use crate::storage::SparseSetIndex; pub use stepping::Stepping; /// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s excluding the current running [`Schedule`]. @@ -1363,14 +1365,22 @@ impl ScheduleGraph { let access_a = system_a.component_access(); let access_b = system_b.component_access(); if !access_a.is_compatible(access_b) { - let conflicts: Vec<_> = access_a - .get_conflicts(access_b) - .into_iter() - .filter(|id| !ignored_ambiguities.contains(id)) - .collect(); - - if !conflicts.is_empty() { - conflicting_systems.push((a, b, conflicts)); + match access_a.get_conflicts(access_b) { + AccessConflicts::Individual(conflicts) => { + let conflicts: Vec<_> = conflicts + .ones() + .map(ComponentId::get_sparse_set_index) + .filter(|id| !ignored_ambiguities.contains(id)) + .collect(); + if !conflicts.is_empty() { + conflicting_systems.push((a, b, conflicts)); + } + } + AccessConflicts::All => { + // there is no specific component conflicting, but the systems are overall incompatible + // for example 2 systems with `Query` + conflicting_systems.push((a, b, Vec::new())); + } } } } diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index 5f6bbf12f6655b..dca9c1542a5519 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -437,6 +437,14 @@ impl BlobVec { } } } + + /// Get the `drop` argument that was passed to `BlobVec::new`. + /// + /// Callers can use this if they have a type-erased pointer of the correct + /// type to add to this [`BlobVec`], which they just want to drop instead. + pub fn get_drop(&self) -> Option)> { + self.drop + } } impl Drop for BlobVec { @@ -513,7 +521,7 @@ mod tests { use crate::{component::Component, ptr::OwningPtr, world::World}; use super::BlobVec; - use std::{alloc::Layout, cell::RefCell, mem, rc::Rc}; + use std::{alloc::Layout, cell::RefCell, mem::align_of, rc::Rc}; unsafe fn drop_ptr(x: OwningPtr<'_>) { // SAFETY: The pointer points to a valid value of type `T` and it is safe to drop this value. @@ -714,7 +722,7 @@ mod tests { for zst in q.iter(&world) { // Ensure that the references returned are properly aligned. assert_eq!( - std::ptr::from_ref::(zst) as usize % mem::align_of::(), + std::ptr::from_ref::(zst) as usize % align_of::(), 0 ); count += 1; diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index 83e533fe609c42..82a1a54d48208d 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,8 +1,10 @@ use crate::archetype::ArchetypeComponentId; -use crate::change_detection::{MutUntyped, TicksMut}; +use crate::change_detection::{MaybeLocation, MaybeUnsafeCellLocation, MutUntyped, TicksMut}; use crate::component::{ComponentId, ComponentTicks, Components, Tick, TickCells}; use crate::storage::{blob_vec::BlobVec, SparseSet}; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; +#[cfg(feature = "track_change_detection")] +use std::panic::Location; use std::{cell::UnsafeCell, mem::ManuallyDrop, thread::ThreadId}; /// The type-erased backing storage and metadata for a single resource within a [`World`]. @@ -17,6 +19,8 @@ pub struct ResourceData { type_name: String, id: ArchetypeComponentId, origin_thread_id: Option, + #[cfg(feature = "track_change_detection")] + changed_by: UnsafeCell<&'static Location<'static>>, } impl Drop for ResourceData { @@ -109,7 +113,9 @@ impl ResourceData { /// If `SEND` is false, this will panic if a value is present and is not accessed from the /// original thread it was inserted in. #[inline] - pub(crate) fn get_with_ticks(&self) -> Option<(Ptr<'_>, TickCells<'_>)> { + pub(crate) fn get_with_ticks( + &self, + ) -> Option<(Ptr<'_>, TickCells<'_>, MaybeUnsafeCellLocation<'_>)> { self.is_present().then(|| { self.validate_access(); ( @@ -119,6 +125,10 @@ impl ResourceData { added: &self.added_ticks, changed: &self.changed_ticks, }, + #[cfg(feature = "track_change_detection")] + &self.changed_by, + #[cfg(not(feature = "track_change_detection"))] + (), ) }) } @@ -129,12 +139,15 @@ impl ResourceData { /// If `SEND` is false, this will panic if a value is present and is not accessed from the /// original thread it was inserted in. pub(crate) fn get_mut(&mut self, last_run: Tick, this_run: Tick) -> Option> { - let (ptr, ticks) = self.get_with_ticks()?; + let (ptr, ticks, _caller) = self.get_with_ticks()?; Some(MutUntyped { // SAFETY: We have exclusive access to the underlying storage. value: unsafe { ptr.assert_unique() }, // SAFETY: We have exclusive access to the underlying storage. ticks: unsafe { TicksMut::from_tick_cells(ticks, last_run, this_run) }, + #[cfg(feature = "track_change_detection")] + // SAFETY: We have exclusive access to the underlying storage. + changed_by: unsafe { _caller.deref_mut() }, }) } @@ -148,7 +161,12 @@ impl ResourceData { /// # Safety /// - `value` must be valid for the underlying type for the resource. #[inline] - pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: Tick) { + pub(crate) unsafe fn insert( + &mut self, + value: OwningPtr<'_>, + change_tick: Tick, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) { if self.is_present() { self.validate_access(); // SAFETY: The caller ensures that the provided value is valid for the underlying type and @@ -165,6 +183,10 @@ impl ResourceData { *self.added_ticks.deref_mut() = change_tick; } *self.changed_ticks.deref_mut() = change_tick; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by.deref_mut() = caller; + } } /// Inserts a value into the resource with a pre-existing change tick. If a @@ -181,6 +203,7 @@ impl ResourceData { &mut self, value: OwningPtr<'_>, change_ticks: ComponentTicks, + #[cfg(feature = "track_change_detection")] caller: &'static Location, ) { if self.is_present() { self.validate_access(); @@ -198,6 +221,10 @@ impl ResourceData { } *self.added_ticks.deref_mut() = change_ticks.added; *self.changed_ticks.deref_mut() = change_ticks.changed; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by.deref_mut() = caller; + } } /// Removes a value from the resource, if present. @@ -207,7 +234,7 @@ impl ResourceData { /// original thread it was inserted from. #[inline] #[must_use = "The returned pointer to the removed component should be used or dropped"] - pub(crate) fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> { + pub(crate) fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks, MaybeLocation)> { if !self.is_present() { return None; } @@ -216,6 +243,13 @@ impl ResourceData { } // 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 + #[cfg(feature = "track_change_detection")] + let caller = unsafe { *self.changed_by.deref_mut() }; + #[cfg(not(feature = "track_change_detection"))] + let caller = (); + // SAFETY: This function is being called through an exclusive mutable reference to Self, which // makes it sound to read these ticks. unsafe { @@ -225,6 +259,7 @@ impl ResourceData { added: self.added_ticks.read(), changed: self.changed_ticks.read(), }, + caller, )) } } @@ -333,6 +368,8 @@ impl Resources { type_name: String::from(component_info.name()), id: f(), origin_thread_id: None, + #[cfg(feature = "track_change_detection")] + changed_by: UnsafeCell::new(Location::caller()) } }) } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 3ef1242c62beaf..6a7b2460a2b4d1 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -1,10 +1,13 @@ use crate::{ + change_detection::MaybeUnsafeCellLocation, component::{ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, entity::Entity, storage::{Column, TableRow}, }; use bevy_ptr::{OwningPtr, Ptr}; use nonmax::NonMaxUsize; +#[cfg(feature = "track_change_detection")] +use std::panic::Location; use std::{cell::UnsafeCell, hash::Hash, marker::PhantomData}; type EntityIndex = u32; @@ -169,14 +172,26 @@ impl ComponentSparseSet { entity: Entity, value: OwningPtr<'_>, change_tick: Tick, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, ) { if let Some(&dense_index) = self.sparse.get(entity.index()) { #[cfg(debug_assertions)] assert_eq!(entity, self.entities[dense_index.as_usize()]); - self.dense.replace(dense_index, value, change_tick); + self.dense.replace( + dense_index, + value, + change_tick, + #[cfg(feature = "track_change_detection")] + caller, + ); } else { let dense_index = self.dense.len(); - self.dense.push(value, ComponentTicks::new(change_tick)); + self.dense.push( + value, + ComponentTicks::new(change_tick), + #[cfg(feature = "track_change_detection")] + caller, + ); self.sparse .insert(entity.index(), TableRow::from_usize(dense_index)); #[cfg(debug_assertions)] @@ -222,7 +237,10 @@ impl ComponentSparseSet { /// /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] - pub fn get_with_ticks(&self, entity: Entity) -> Option<(Ptr<'_>, TickCells<'_>)> { + pub fn get_with_ticks( + &self, + entity: Entity, + ) -> Option<(Ptr<'_>, TickCells<'_>, MaybeUnsafeCellLocation<'_>)> { let dense_index = *self.sparse.get(entity.index())?; #[cfg(debug_assertions)] assert_eq!(entity, self.entities[dense_index.as_usize()]); @@ -234,6 +252,10 @@ impl ComponentSparseSet { added: self.dense.get_added_tick_unchecked(dense_index), changed: self.dense.get_changed_tick_unchecked(dense_index), }, + #[cfg(feature = "track_change_detection")] + self.dense.get_changed_by_unchecked(dense_index), + #[cfg(not(feature = "track_change_detection"))] + (), )) } } @@ -274,6 +296,22 @@ impl ComponentSparseSet { unsafe { Some(self.dense.get_ticks_unchecked(dense_index)) } } + /// Returns a reference to the calling location that last changed the entity's component value. + /// + /// Returns `None` if `entity` does not have a component in the sparse set. + #[inline] + #[cfg(feature = "track_change_detection")] + pub fn get_changed_by( + &self, + entity: Entity, + ) -> Option<&UnsafeCell<&'static Location<'static>>> { + let dense_index = *self.sparse.get(entity.index())?; + #[cfg(debug_assertions)] + assert_eq!(entity, self.entities[dense_index.as_usize()]); + // SAFETY: if the sparse index points to something in the dense vec, it exists + unsafe { Some(self.dense.get_changed_by_unchecked(dense_index)) } + } + /// Removes the `entity` from this sparse set and returns a pointer to the associated value (if /// it exists). #[must_use = "The returned pointer must be used to drop the removed component."] @@ -284,7 +322,7 @@ impl ComponentSparseSet { self.entities.swap_remove(dense_index.as_usize()); let is_last = dense_index.as_usize() == self.dense.len() - 1; // SAFETY: dense_index was just removed from `sparse`, which ensures that it is valid - let (value, _) = unsafe { self.dense.swap_remove_and_forget_unchecked(dense_index) }; + let (value, _, _) = unsafe { self.dense.swap_remove_and_forget_unchecked(dense_index) }; if !is_last { let swapped_entity = self.entities[dense_index.as_usize()]; #[cfg(not(debug_assertions))] diff --git a/crates/bevy_ecs/src/storage/table.rs b/crates/bevy_ecs/src/storage/table.rs index 8fc73bb873abb8..d525cebe210bda 100644 --- a/crates/bevy_ecs/src/storage/table.rs +++ b/crates/bevy_ecs/src/storage/table.rs @@ -1,4 +1,5 @@ use crate::{ + change_detection::{MaybeLocation, MaybeUnsafeCellLocation}, component::{ComponentId, ComponentInfo, ComponentTicks, Components, Tick, TickCells}, entity::Entity, query::DebugCheckedUnwrap, @@ -7,6 +8,8 @@ use crate::{ use bevy_ptr::{OwningPtr, Ptr, PtrMut, UnsafeCellDeref}; use bevy_utils::HashMap; use std::alloc::Layout; +#[cfg(feature = "track_change_detection")] +use std::panic::Location; use std::{ cell::UnsafeCell, ops::{Index, IndexMut}, @@ -152,6 +155,8 @@ pub struct Column { data: BlobVec, added_ticks: Vec>, changed_ticks: Vec>, + #[cfg(feature = "track_change_detection")] + changed_by: Vec>>, } impl Column { @@ -163,6 +168,8 @@ impl Column { data: unsafe { BlobVec::new(component_info.layout(), component_info.drop(), capacity) }, added_ticks: Vec::with_capacity(capacity), changed_ticks: Vec::with_capacity(capacity), + #[cfg(feature = "track_change_detection")] + changed_by: Vec::with_capacity(capacity), } } @@ -179,7 +186,13 @@ impl Column { /// # Safety /// Assumes data has already been allocated for the given row. #[inline] - pub(crate) unsafe fn initialize(&mut self, row: TableRow, data: OwningPtr<'_>, tick: Tick) { + pub(crate) unsafe fn initialize( + &mut self, + row: TableRow, + data: OwningPtr<'_>, + tick: Tick, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) { debug_assert!(row.as_usize() < self.len()); self.data.initialize_unchecked(row.as_usize(), data); *self.added_ticks.get_unchecked_mut(row.as_usize()).get_mut() = tick; @@ -187,6 +200,10 @@ impl Column { .changed_ticks .get_unchecked_mut(row.as_usize()) .get_mut() = tick; + #[cfg(feature = "track_change_detection")] + { + *self.changed_by.get_unchecked_mut(row.as_usize()).get_mut() = caller; + } } /// Writes component data to the column at given row. @@ -195,13 +212,38 @@ impl Column { /// # Safety /// Assumes data has already been allocated for the given row. #[inline] - pub(crate) unsafe fn replace(&mut self, row: TableRow, data: OwningPtr<'_>, change_tick: Tick) { + pub(crate) unsafe fn replace( + &mut self, + row: TableRow, + data: OwningPtr<'_>, + change_tick: Tick, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) { debug_assert!(row.as_usize() < self.len()); self.data.replace_unchecked(row.as_usize(), data); *self .changed_ticks .get_unchecked_mut(row.as_usize()) .get_mut() = change_tick; + + #[cfg(feature = "track_change_detection")] + { + *self.changed_by.get_unchecked_mut(row.as_usize()).get_mut() = caller; + } + } + + /// Call [`drop`] on a value. + /// + /// # Safety + /// `data` must point to the same type that this table stores, so the + /// correct drop function is called. + #[inline] + pub(crate) unsafe fn drop(&self, data: OwningPtr<'_>) { + if let Some(drop) = self.data.get_drop() { + // Safety: we're using the same drop fn that the BlobVec would + // if we inserted the data instead of dropping it. + unsafe { drop(data) } + } } /// Gets the current number of elements stored in the column. @@ -231,6 +273,8 @@ impl Column { self.data.swap_remove_and_drop_unchecked(row.as_usize()); self.added_ticks.swap_remove(row.as_usize()); self.changed_ticks.swap_remove(row.as_usize()); + #[cfg(feature = "track_change_detection")] + self.changed_by.swap_remove(row.as_usize()); } /// Removes an element from the [`Column`] and returns it and its change detection ticks. @@ -249,11 +293,15 @@ impl Column { pub(crate) unsafe fn swap_remove_and_forget_unchecked( &mut self, row: TableRow, - ) -> (OwningPtr<'_>, ComponentTicks) { + ) -> (OwningPtr<'_>, ComponentTicks, MaybeLocation) { let data = self.data.swap_remove_and_forget_unchecked(row.as_usize()); let added = self.added_ticks.swap_remove(row.as_usize()).into_inner(); let changed = self.changed_ticks.swap_remove(row.as_usize()).into_inner(); - (data, ComponentTicks { added, changed }) + #[cfg(feature = "track_change_detection")] + let caller = self.changed_by.swap_remove(row.as_usize()).into_inner(); + #[cfg(not(feature = "track_change_detection"))] + let caller = (); + (data, ComponentTicks { added, changed }, caller) } /// Removes the element from `other` at `src_row` and inserts it @@ -281,16 +329,28 @@ impl Column { other.added_ticks.swap_remove(src_row.as_usize()); *self.changed_ticks.get_unchecked_mut(dst_row.as_usize()) = other.changed_ticks.swap_remove(src_row.as_usize()); + #[cfg(feature = "track_change_detection")] + { + *self.changed_by.get_unchecked_mut(dst_row.as_usize()) = + other.changed_by.swap_remove(src_row.as_usize()); + } } /// Pushes a new value onto the end of the [`Column`]. /// /// # Safety /// `ptr` must point to valid data of this column's component type - pub(crate) unsafe fn push(&mut self, ptr: OwningPtr<'_>, ticks: ComponentTicks) { + pub(crate) unsafe fn push( + &mut self, + ptr: OwningPtr<'_>, + ticks: ComponentTicks, + #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + ) { self.data.push(ptr); self.added_ticks.push(UnsafeCell::new(ticks.added)); self.changed_ticks.push(UnsafeCell::new(ticks.changed)); + #[cfg(feature = "track_change_detection")] + self.changed_by.push(UnsafeCell::new(caller)); } #[inline] @@ -298,6 +358,8 @@ impl Column { self.data.reserve_exact(additional); self.added_ticks.reserve_exact(additional); self.changed_ticks.reserve_exact(additional); + #[cfg(feature = "track_change_detection")] + self.changed_by.reserve_exact(additional); } /// Fetches the data pointer to the first element of the [`Column`]. @@ -342,11 +404,25 @@ impl Column { &self.changed_ticks } + /// Fetches the slice to the [`Column`]'s caller locations. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + #[inline] + #[cfg(feature = "track_change_detection")] + pub fn get_changed_by_slice(&self) -> &[UnsafeCell<&'static Location<'static>>] { + &self.changed_by + } + /// Fetches a reference to the data and change detection ticks at `row`. /// /// Returns `None` if `row` is out of bounds. #[inline] - pub fn get(&self, row: TableRow) -> Option<(Ptr<'_>, TickCells<'_>)> { + pub fn get( + &self, + row: TableRow, + ) -> Option<(Ptr<'_>, TickCells<'_>, MaybeUnsafeCellLocation<'_>)> { (row.as_usize() < self.data.len()) // SAFETY: The row is length checked before fetching the pointer. This is being // accessed through a read-only reference to the column. @@ -357,6 +433,10 @@ impl Column { added: self.added_ticks.get_unchecked(row.as_usize()), changed: self.changed_ticks.get_unchecked(row.as_usize()), }, + #[cfg(feature = "track_change_detection")] + self.changed_by.get_unchecked(row.as_usize()), + #[cfg(not(feature = "track_change_detection"))] + (), ) }) } @@ -483,6 +563,35 @@ impl Column { } } + /// Fetches the calling location that last changed the value at `row`. + /// + /// Returns `None` if `row` is out of bounds. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + #[inline] + #[cfg(feature = "track_change_detection")] + pub fn get_changed_by(&self, row: TableRow) -> Option<&UnsafeCell<&'static Location<'static>>> { + self.changed_by.get(row.as_usize()) + } + + /// Fetches the calling location that last changed the value at `row`. + /// + /// Unlike [`Column::get_changed_by`] this function does not do any bounds checking. + /// + /// # Safety + /// `row` must be within the range `[0, self.len())`. + #[inline] + #[cfg(feature = "track_change_detection")] + pub unsafe fn get_changed_by_unchecked( + &self, + row: TableRow, + ) -> &UnsafeCell<&'static Location<'static>> { + debug_assert!(row.as_usize() < self.changed_by.len()); + self.changed_by.get_unchecked(row.as_usize()) + } + /// Clears the column, removing all values. /// /// Note that this function has no effect on the allocated capacity of the [`Column`]> @@ -490,6 +599,8 @@ impl Column { self.data.clear(); self.added_ticks.clear(); self.changed_ticks.clear(); + #[cfg(feature = "track_change_detection")] + self.changed_by.clear(); } #[inline] @@ -608,7 +719,7 @@ impl Table { new_column.initialize_from_unchecked(column, row, new_row); } else { // It's the caller's responsibility to drop these cases. - let (_, _) = column.swap_remove_and_forget_unchecked(row); + let (_, _, _) = column.swap_remove_and_forget_unchecked(row); } } TableMoveResult { @@ -740,6 +851,8 @@ impl Table { column.data.set_len(self.entities.len()); column.added_ticks.push(UnsafeCell::new(Tick::new(0))); column.changed_ticks.push(UnsafeCell::new(Tick::new(0))); + #[cfg(feature = "track_change_detection")] + column.changed_by.push(UnsafeCell::new(Location::caller())); } TableRow::from_usize(index) } @@ -924,6 +1037,9 @@ mod tests { entity::Entity, storage::{TableBuilder, TableRow}, }; + #[cfg(feature = "track_change_detection")] + use std::panic::Location; + #[derive(Component)] struct W(T); @@ -947,6 +1063,8 @@ mod tests { row, value_ptr, Tick::new(0), + #[cfg(feature = "track_change_detection")] + Location::caller(), ); }); }; diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index f3daf8fb06e31e..520d73f4e9cdc4 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -1,18 +1,24 @@ -use bevy_utils::all_tuples; +use bevy_utils::{all_tuples, synccell::SyncCell}; -use super::{ - BuildableSystemParam, FunctionSystem, Local, Res, ResMut, Resource, SystemMeta, SystemParam, - SystemParamFunction, SystemState, +use crate::{ + prelude::QueryBuilder, + query::{QueryData, QueryFilter, QueryState}, + system::{ + system_param::{Local, ParamSet, SystemParam}, + Query, SystemMeta, + }, + world::{FromWorld, World}, }; -use crate::prelude::{FromWorld, Query, World}; -use crate::query::{QueryData, QueryFilter}; +use std::fmt::Debug; -/// Builder struct used to construct state for [`SystemParam`] passed to a system. +use super::{init_query_param, Res, ResMut, Resource, SystemState}; + +/// A builder that can create a [`SystemParam`] /// /// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_ecs_macros::SystemParam; -/// # use bevy_ecs::system::RunSystemOnce; +/// # use bevy_ecs::system::{RunSystemOnce, ParamBuilder, LocalBuilder, QueryParamBuilder}; /// # /// # #[derive(Component)] /// # struct A; @@ -28,121 +34,239 @@ use crate::query::{QueryData, QueryFilter}; /// # /// # let mut world = World::new(); /// # world.insert_resource(R); -/// +/// # /// fn my_system(res: Res, query: Query<&A>, param: MyParam) { /// // ... /// } /// -/// // Create a builder from the world, helper methods exist to add `SystemParam`, -/// // alternatively use `.param::()` for any other `SystemParam` types. -/// let system = SystemBuilder::<()>::new(&mut world) -/// .resource::() -/// .query::<&A>() -/// .param::() -/// .build(my_system); +/// // To build a system, create a tuple of `SystemParamBuilder`s with a builder for each param. +/// // `ParamBuilder` can be used to build a parameter using its default initialization, +/// // and has helper methods to create typed builders. +/// let system = ( +/// ParamBuilder, +/// ParamBuilder::query::<&A>(), +/// ParamBuilder::of::(), +/// ) +/// .build_state(&mut world) +/// .build_system(my_system); /// -/// // Parameters that the builder is initialised with will appear first in the arguments. -/// let system = SystemBuilder::<(Res, Query<&A>)>::new(&mut world) -/// .param::() -/// .build(my_system); +/// // Other implementations of `SystemParamBuilder` can be used to configure the parameters. +/// let system = ( +/// ParamBuilder, +/// QueryParamBuilder::new::<&A, ()>(|builder| { +/// builder.with::(); +/// }), +/// ParamBuilder, +/// ) +/// .build_state(&mut world) +/// .build_system(my_system); +/// +/// fn single_parameter_system(local: Local) { +/// // ... +/// } /// -/// // Parameters that implement `BuildableSystemParam` can use `.builder::()` to build in place. -/// let system = SystemBuilder::<()>::new(&mut world) -/// .resource::() -/// .builder::>(|builder| { builder.with::(); }) -/// .param::() -/// .build(my_system); +/// // Note that the builder for a system must be a tuple, even if there is only one parameter. +/// let system = (LocalBuilder(2),) +/// .build_state(&mut world) +/// .build_system(single_parameter_system); /// /// world.run_system_once(system); ///``` -pub struct SystemBuilder<'w, T: SystemParam = ()> { - pub(crate) meta: SystemMeta, - pub(crate) state: T::State, - pub(crate) world: &'w mut World, +/// +/// # Safety +/// +/// The implementor must ensure the following is true. +/// - [`SystemParamBuilder::build`] correctly registers all [`World`] accesses used +/// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). +/// - None of the world accesses may conflict with any prior accesses registered +/// on `system_meta`. +/// +/// Note that this depends on the implementation of [`SystemParam::get_param`], +/// so if `Self` is not a local type then you must call [`SystemParam::init_state`] +/// or another [`SystemParamBuilder::build`] +pub unsafe trait SystemParamBuilder: Sized { + /// Registers any [`World`] access used by this [`SystemParam`] + /// and creates a new instance of this param's [`State`](SystemParam::State). + fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State; + + /// Create a [`SystemState`] from a [`SystemParamBuilder`]. + /// To create a system, call [`SystemState::build_system`] on the result. + fn build_state(self, world: &mut World) -> SystemState

{ + SystemState::from_builder(world, self) + } } -impl<'w, T: SystemParam> SystemBuilder<'w, T> { - /// Construct a new builder with the default state for `T` - pub fn new(world: &'w mut World) -> Self { - let mut meta = SystemMeta::new::(); - Self { - state: T::init_state(world, &mut meta), - meta, - world, - } +/// A [`SystemParamBuilder`] for any [`SystemParam`] that uses its default initialization. +#[derive(Default, Debug, Copy, Clone)] +pub struct ParamBuilder; + +// SAFETY: Calls `SystemParam::init_state` +unsafe impl SystemParamBuilder

for ParamBuilder { + fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State { + P::init_state(world, meta) } +} - /// Construct the a system with the built params - pub fn build(self, func: F) -> FunctionSystem - where - F: SystemParamFunction, +impl ParamBuilder { + /// Creates a [`SystemParamBuilder`] for any [`SystemParam`] that uses its default initialization. + pub fn of() -> impl SystemParamBuilder { + Self + } + + /// Helper method for reading a [`Resource`] as a param, equivalent to `of::>()` + pub fn resource<'w, T: Resource>() -> impl SystemParamBuilder> { + Self + } + + /// Helper method for mutably accessing a [`Resource`] as a param, equivalent to `of::>()` + pub fn resource_mut<'w, T: Resource>() -> impl SystemParamBuilder> { + Self + } + + /// Helper method for adding a [`Local`] as a param, equivalent to `of::>()` + pub fn local<'s, T: FromWorld + Send + 'static>() -> impl SystemParamBuilder> { + Self + } + + /// Helper method for adding a [`Query`] as a param, equivalent to `of::>()` + pub fn query<'w, 's, D: QueryData + 'static>() -> impl SystemParamBuilder> { - FunctionSystem::from_builder(self, func) + Self } - /// Return the constructed [`SystemState`] - pub fn state(self) -> SystemState { - SystemState::from_builder(self) + /// Helper method for adding a filtered [`Query`] as a param, equivalent to `of::>()` + pub fn query_filtered<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static>( + ) -> impl SystemParamBuilder> { + Self } } -macro_rules! impl_system_builder { - ($($curr: ident),*) => { - impl<'w, $($curr: SystemParam,)*> SystemBuilder<'w, ($($curr,)*)> { - /// Add `T` as a parameter built from the world - pub fn param(mut self) -> SystemBuilder<'w, ($($curr,)* T,)> { - #[allow(non_snake_case)] - let ($($curr,)*) = self.state; - SystemBuilder { - state: ($($curr,)* T::init_state(self.world, &mut self.meta),), - meta: self.meta, - world: self.world, - } - } +// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> + SystemParamBuilder> for QueryState +{ + fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + self.validate_world(world.id()); + init_query_param(world, system_meta, &self); + self + } +} - /// Helper method for reading a [`Resource`] as a param, equivalent to `.param::>()` - pub fn resource(self) -> SystemBuilder<'w, ($($curr,)* Res<'static, T>,)> { - self.param::>() - } +/// A [`SystemParamBuilder`] for a [`Query`]. +pub struct QueryParamBuilder(T); - /// Helper method for mutably accessing a [`Resource`] as a param, equivalent to `.param::>()` - pub fn resource_mut(self) -> SystemBuilder<'w, ($($curr,)* ResMut<'static, T>,)> { - self.param::>() - } +impl QueryParamBuilder { + /// Creates a [`SystemParamBuilder`] for a [`Query`] that accepts a callback to configure the [`QueryBuilder`]. + pub fn new(f: T) -> Self + where + T: FnOnce(&mut QueryBuilder), + { + Self(f) + } +} - /// Helper method for adding a [`Local`] as a param, equivalent to `.param::>()` - pub fn local(self) -> SystemBuilder<'w, ($($curr,)* Local<'static, T>,)> { - self.param::>() - } +impl<'a, D: QueryData, F: QueryFilter> + QueryParamBuilder) + 'a>> +{ + /// Creates a [`SystemParamBuilder`] for a [`Query`] that accepts a callback to configure the [`QueryBuilder`]. + /// This boxes the callback so that it has a common type and can be put in a `Vec`. + pub fn new_box(f: impl FnOnce(&mut QueryBuilder) + 'a) -> Self { + Self(Box::new(f)) + } +} - /// Helper method for adding a [`Query`] as a param, equivalent to `.param::>()` - pub fn query(self) -> SystemBuilder<'w, ($($curr,)* Query<'static, 'static, D, ()>,)> { - self.query_filtered::() - } +// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +unsafe impl< + 'w, + 's, + D: QueryData + 'static, + F: QueryFilter + 'static, + T: FnOnce(&mut QueryBuilder), + > SystemParamBuilder> for QueryParamBuilder +{ + fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + let mut builder = QueryBuilder::new(world); + (self.0)(&mut builder); + let state = builder.build(); + init_query_param(world, system_meta, &state); + state + } +} - /// Helper method for adding a filtered [`Query`] as a param, equivalent to `.param::>()` - pub fn query_filtered(self) -> SystemBuilder<'w, ($($curr,)* Query<'static, 'static, D, F>,)> { - self.param::>() +macro_rules! impl_system_param_builder_tuple { + ($(($param: ident, $builder: ident)),*) => { + // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls + unsafe impl<$($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder<($($param,)*)> for ($($builder,)*) { + fn build(self, _world: &mut World, _meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + #[allow(non_snake_case)] + let ($($builder,)*) = self; + #[allow(clippy::unused_unit)] + ($($builder.build(_world, _meta),)*) } + } + }; +} - /// Add `T` as a parameter built with the given function - pub fn builder( - mut self, - func: impl FnOnce(&mut T::Builder<'_>), - ) -> SystemBuilder<'w, ($($curr,)* T,)> { - #[allow(non_snake_case)] - let ($($curr,)*) = self.state; - SystemBuilder { - state: ($($curr,)* T::build(self.world, &mut self.meta, func),), - meta: self.meta, - world: self.world, +all_tuples!(impl_system_param_builder_tuple, 0, 16, P, B); + +/// A [`SystemParamBuilder`] for a [`ParamSet`]. +/// To build a [`ParamSet`] with a tuple of system parameters, pass a tuple of matching [`SystemParamBuilder`]s. +/// To build a [`ParamSet`] with a `Vec` of system parameters, pass a `Vec` of matching [`SystemParamBuilder`]s. +pub struct ParamSetBuilder(T); + +macro_rules! impl_param_set_builder_tuple { + ($(($param: ident, $builder: ident, $meta: ident)),*) => { + // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls + unsafe impl<'w, 's, $($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder> for ParamSetBuilder<($($builder,)*)> { + #[allow(non_snake_case)] + fn build(self, _world: &mut World, _system_meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + let ParamSetBuilder(($($builder,)*)) = self; + // Note that this is slightly different from `init_state`, which calls `init_state` on each param twice. + // One call populates an empty `SystemMeta` with the new access, while the other runs against a cloned `SystemMeta` to check for conflicts. + // Builders can only be invoked once, so we do both in a single call here. + // That means that any `filtered_accesses` in the `component_access_set` will get copied to every `$meta` + // and will appear multiple times in the final `SystemMeta`. + $( + let mut $meta = _system_meta.clone(); + let $param = $builder.build(_world, &mut $meta); + )* + // Make the ParamSet non-send if any of its parameters are non-send. + if false $(|| !$meta.is_send())* { + _system_meta.set_non_send(); } + $( + _system_meta + .component_access_set + .extend($meta.component_access_set); + _system_meta + .archetype_component_access + .extend(&$meta.archetype_component_access); + )* + #[allow(clippy::unused_unit)] + ($($param,)*) } } }; } -all_tuples!(impl_system_builder, 0, 15, P); +all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta); + +/// A [`SystemParamBuilder`] for a [`Local`]. +/// The provided value will be used as the initial value of the `Local`. +pub struct LocalBuilder(pub T); + +// SAFETY: `Local` performs no world access. +unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> + for LocalBuilder +{ + fn build( + self, + _world: &mut World, + _meta: &mut SystemMeta, + ) -> as SystemParam>::State { + SyncCell::new(self.0) + } +} #[cfg(test)] mod tests { @@ -155,6 +279,12 @@ mod tests { #[derive(Component)] struct A; + #[derive(Component)] + struct B; + + #[derive(Component)] + struct C; + fn local_system(local: Local) -> u64 { *local } @@ -171,9 +301,9 @@ mod tests { fn local_builder() { let mut world = World::new(); - let system = SystemBuilder::<()>::new(&mut world) - .builder::>(|x| *x = 10) - .build(local_system); + let system = (LocalBuilder(10),) + .build_state(&mut world) + .build_system(local_system); let result = world.run_system_once(system); assert_eq!(result, 10); @@ -186,11 +316,26 @@ mod tests { world.spawn(A); world.spawn_empty(); - let system = SystemBuilder::<()>::new(&mut world) - .builder::>(|query| { - query.with::(); - }) - .build(query_system); + let system = (QueryParamBuilder::new(|query| { + query.with::(); + }),) + .build_state(&mut world) + .build_system(query_system); + + let result = world.run_system_once(system); + assert_eq!(result, 1); + } + + #[test] + fn query_builder_state() { + let mut world = World::new(); + + world.spawn(A); + world.spawn_empty(); + + let state = QueryBuilder::new(&mut world).with::().build(); + + let system = (state,).build_state(&mut world).build_system(query_system); let result = world.run_system_once(system); assert_eq!(result, 1); @@ -203,12 +348,38 @@ mod tests { world.spawn(A); world.spawn_empty(); - let system = SystemBuilder::<()>::new(&mut world) - .local::() - .param::>() - .build(multi_param_system); + let system = (LocalBuilder(0), ParamBuilder) + .build_state(&mut world) + .build_system(multi_param_system); let result = world.run_system_once(system); assert_eq!(result, 1); } + + #[test] + fn param_set_builder() { + let mut world = World::new(); + + world.spawn((A, B, C)); + world.spawn((A, B)); + world.spawn((A, C)); + world.spawn((A, C)); + world.spawn_empty(); + + let system = (ParamSetBuilder(( + QueryParamBuilder::new(|builder| { + builder.with::(); + }), + QueryParamBuilder::new(|builder| { + builder.with::(); + }), + )),) + .build_state(&mut world) + .build_system(|mut params: ParamSet<(Query<&mut A>, Query<&mut A>)>| { + params.p0().iter().count() + params.p1().iter().count() + }); + + let result = world.run_system_once(system); + assert_eq!(result, 5); + } } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index e1de10b3943f38..cec961ea0abdc4 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -1,6 +1,4 @@ -use std::{borrow::Cow, cell::UnsafeCell, marker::PhantomData}; - -use bevy_ptr::UnsafeCellDeref; +use std::{borrow::Cow, marker::PhantomData}; use crate::{ archetype::ArchetypeComponentId, @@ -182,18 +180,16 @@ where ) } - fn run<'w>(&mut self, input: Self::In, world: &'w mut World) -> Self::Out { - // SAFETY: Converting `&mut T` -> `&UnsafeCell` - // is explicitly allowed in the docs for `UnsafeCell`. - let world: &'w UnsafeCell = unsafe { std::mem::transmute(world) }; + fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out { + let world = world.as_unsafe_world_cell(); Func::combine( input, // SAFETY: Since these closures are `!Send + !Sync + !'static`, they can never // be called in parallel. Since mutable access to `world` only exists within // the scope of either closure, we can be sure they will never alias one another. - |input| self.a.run(input, unsafe { world.deref_mut() }), + |input| self.a.run(input, unsafe { world.world_mut() }), #[allow(clippy::undocumented_unsafe_blocks)] - |input| self.b.run(input, unsafe { world.deref_mut() }), + |input| self.b.run(input, unsafe { world.world_mut() }), ) } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 39284356ed632f..fe5eda1b53fb3b 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,17 +1,22 @@ mod parallel_scope; +use core::panic::Location; + use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource}; use crate::{ self as bevy_ecs, - bundle::Bundle, + bundle::{Bundle, InsertMode}, component::{ComponentId, ComponentInfo}, entity::{Entities, Entity}, event::Event, observer::{Observer, TriggerEvent, TriggerTargets}, system::{RunSystemWithInput, SystemId}, - world::command_queue::RawCommandQueue, - world::{Command, CommandQueue, EntityWorldMut, FromWorld, World}, + world::{ + command_queue::RawCommandQueue, Command, CommandQueue, EntityWorldMut, FromWorld, + SpawnBatchIter, World, + }, }; +use bevy_ptr::OwningPtr; use bevy_utils::tracing::{error, info}; pub use parallel_scope::*; @@ -71,6 +76,12 @@ pub struct Commands<'w, 's> { entities: &'w Entities, } +// SAFETY: All commands [`Command`] implement [`Send`] +unsafe impl Send for Commands<'_, '_> {} + +// SAFETY: `Commands` never gives access to the inner commands. +unsafe impl Sync for Commands<'_, '_> {} + const _: () = { type __StructFieldsAlias<'w, 's> = (Deferred<'s, CommandQueue>, &'w Entities); #[doc(hidden)] @@ -82,7 +93,7 @@ const _: () = { type State = FetchState; type Item<'w, 's> = Commands<'w, 's>; fn init_state( - world: &mut bevy_ecs::world::World, + world: &mut World, system_meta: &mut bevy_ecs::system::SystemMeta, ) -> Self::State { FetchState { @@ -109,7 +120,7 @@ const _: () = { fn apply( state: &mut Self::State, system_meta: &bevy_ecs::system::SystemMeta, - world: &mut bevy_ecs::world::World, + world: &mut World, ) { <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::apply( &mut state.state, @@ -235,8 +246,7 @@ impl<'w, 's> Commands<'w, 's> { } } - /// Pushes a [`Command`] to the queue for creating a new empty [`Entity`], - /// and returns its corresponding [`EntityCommands`]. + /// Reserves a new empty [`Entity`] to be spawned, and returns its corresponding [`EntityCommands`]. /// /// See [`World::spawn_empty`] for more details. /// @@ -352,6 +362,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// - [`spawn_empty`](Self::spawn_empty) to spawn an entity without any components. /// - [`spawn_batch`](Self::spawn_batch) to spawn entities with a bundle each. + #[track_caller] pub fn spawn(&mut self, bundle: T) -> EntityCommands { let mut e = self.spawn_empty(); e.insert(bundle); @@ -488,6 +499,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// - [`spawn`](Self::spawn) to spawn an entity with a bundle. /// - [`spawn_empty`](Self::spawn_empty) to spawn an entity without any components. + #[track_caller] pub fn spawn_batch(&mut self, bundles_iter: I) where I: IntoIterator + Send + Sync + 'static, @@ -533,6 +545,7 @@ 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). + #[track_caller] pub fn insert_or_spawn_batch(&mut self, bundles_iter: I) where I: IntoIterator + Send + Sync + 'static, @@ -565,6 +578,7 @@ impl<'w, 's> Commands<'w, 's> { /// # } /// # bevy_ecs::system::assert_is_system(initialise_scoreboard); /// ``` + #[track_caller] pub fn init_resource(&mut self) { self.push(init_resource::); } @@ -594,6 +608,7 @@ impl<'w, 's> Commands<'w, 's> { /// # } /// # bevy_ecs::system::assert_is_system(system); /// ``` + #[track_caller] pub fn insert_resource(&mut self, resource: R) { self.push(insert_resource(resource)); } @@ -760,11 +775,15 @@ impl<'w, 's> Commands<'w, 's> { /// watches those targets. /// /// [`Trigger`]: crate::observer::Trigger - pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) { + pub fn trigger_targets( + &mut self, + event: impl Event, + targets: impl TriggerTargets + Send + Sync + 'static, + ) { self.add(TriggerEvent { event, targets }); } - /// Spawn an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer. + /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer. pub fn observe( &mut self, observer: impl IntoObserverSystem, @@ -882,6 +901,7 @@ impl EntityCommands<'_> { /// Adds a [`Bundle`] of components to the entity. /// /// This will overwrite any previous value(s) of the same component type. + /// See [`EntityCommands::insert_if_new`] to keep the old value instead. /// /// # Panics /// @@ -930,8 +950,72 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` + #[track_caller] pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.add(insert(bundle)) + self.add(insert(bundle, InsertMode::Replace)) + } + + /// Adds a [`Bundle`] of components to the entity without overwriting. + /// + /// This is the same as [`EntityCommands::insert`], but in case of duplicate + /// components will leave the old values instead of replacing them with new + /// ones. + /// + /// # Panics + /// + /// The command will panic when applied if the associated entity does not + /// exist. + /// + /// To avoid a panic in this case, use the command [`Self::try_insert_if_new`] + /// instead. + pub fn insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { + self.add(insert(bundle, InsertMode::Keep)) + } + + /// Adds a dynamic component to an entity. + /// + /// See [`EntityWorldMut::insert_by_id`] for more information. + /// + /// # Panics + /// + /// The command will panic when applied if the associated entity does not exist. + /// + /// To avoid a panic in this case, use the command [`Self::try_insert_by_id`] instead. + /// + /// # Safety + /// + /// - [`ComponentId`] must be from the same world as `self`. + /// - `T` must have the same layout as the one passed during `component_id` creation. + #[track_caller] + pub unsafe fn insert_by_id( + &mut self, + component_id: ComponentId, + value: T, + ) -> &mut Self { + let caller = Location::caller(); + // SAFETY: same invariants as parent call + self.add(unsafe {insert_by_id(component_id, value, move |entity| { + panic!("error[B0003]: {caller}: Could not insert a component {component_id:?} (with type {}) for entity {entity:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/#b0003", std::any::type_name::()); + })}); + self + } + + /// Attempts to add a dynamic component to an entity. + /// + /// See [`EntityWorldMut::insert_by_id`] for more information. + /// + /// # Safety + /// + /// - [`ComponentId`] must be from the same world as `self`. + /// - `T` must have the same layout as the one passed during `component_id` creation. + pub unsafe fn try_insert_by_id( + &mut self, + component_id: ComponentId, + value: T, + ) -> &mut Self { + // SAFETY: same invariants as parent call + self.add(unsafe { insert_by_id(component_id, value, |_| {}) }); + self } /// Tries to add a [`Bundle`] of components to the entity. @@ -982,8 +1066,22 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` + #[track_caller] pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.add(try_insert(bundle)) + self.add(try_insert(bundle, InsertMode::Replace)) + } + + /// Tries to add a [`Bundle`] of components to the entity without overwriting. + /// + /// This is the same as [`EntityCommands::try_insert`], but in case of duplicate + /// components will leave the old values instead of replacing them with new + /// ones. + /// + /// # Note + /// + /// Unlike [`Self::insert_if_new`], this will not panic if the associated entity does not exist. + pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { + self.add(try_insert(bundle, InsertMode::Keep)) } /// Removes a [`Bundle`] of components from the entity. @@ -1065,8 +1163,9 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(remove_character_system); /// ``` + #[track_caller] pub fn despawn(&mut self) { - self.add(despawn); + self.add(despawn()); } /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. @@ -1148,6 +1247,15 @@ impl EntityCommands<'_> { self.commands.reborrow() } + /// Sends a [`Trigger`] targeting this entity. This will run any [`Observer`] of the `event` that + /// watches this entity. + /// + /// [`Trigger`]: crate::observer::Trigger + pub fn trigger(&mut self, event: impl Event) -> &mut Self { + self.commands.trigger_targets(event, self.entity); + self + } + /// Creates an [`Observer`] listening for a trigger of type `T` that targets this entity. pub fn observe( &mut self, @@ -1188,13 +1296,21 @@ where /// A [`Command`] that consumes an iterator of [`Bundle`]s to spawn a series of entities. /// /// This is more efficient than spawning the entities individually. +#[track_caller] fn spawn_batch(bundles_iter: I) -> impl Command where I: IntoIterator + Send + Sync + 'static, B: Bundle, { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); move |world: &mut World| { - world.spawn_batch(bundles_iter); + SpawnBatchIter::new( + world, + bundles_iter.into_iter(), + #[cfg(feature = "track_change_detection")] + caller, + ); } } @@ -1202,13 +1318,20 @@ where /// If any entities do not already exist in the world, they will be spawned. /// /// This is more efficient than inserting the bundles individually. +#[track_caller] fn insert_or_spawn_batch(bundles_iter: I) -> impl Command where I: IntoIterator + Send + Sync + 'static, B: Bundle, { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); move |world: &mut World| { - if let Err(invalid_entities) = world.insert_or_spawn_batch(bundles_iter) { + if let Err(invalid_entities) = world.insert_or_spawn_batch_with_caller( + bundles_iter, + #[cfg(feature = "track_change_detection")] + caller, + ) { error!( "Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", std::any::type_name::(), @@ -1225,26 +1348,71 @@ 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. -fn despawn(entity: Entity, world: &mut World) { - world.despawn(entity); +#[track_caller] +fn despawn() -> impl EntityCommand { + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + world.despawn_with_caller(entity, caller); + } } /// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity. -fn insert(bundle: T) -> impl EntityCommand { +#[track_caller] +fn insert(bundle: T, mode: InsertMode) -> impl EntityCommand { + let caller = Location::caller(); move |entity: Entity, world: &mut World| { if let Some(mut entity) = world.get_entity_mut(entity) { - entity.insert(bundle); + entity.insert_with_caller( + bundle, + mode, + #[cfg(feature = "track_change_detection")] + caller, + ); } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", std::any::type_name::(), entity); + panic!("error[B0003]: {caller}: Could not insert a bundle (of type `{}`) for entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", std::any::type_name::(), entity); } } } /// An [`EntityCommand`] that attempts to add the components in a [`Bundle`] to an entity. -fn try_insert(bundle: impl Bundle) -> impl EntityCommand { +/// Does nothing if the entity does not exist. +#[track_caller] +fn try_insert(bundle: impl Bundle, mode: InsertMode) -> impl EntityCommand { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); + move |entity, world: &mut World| { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.insert_with_caller( + bundle, + mode, + #[cfg(feature = "track_change_detection")] + caller, + ); + } + } +} + +/// An [`EntityCommand`] that attempts to add the dynamic component to an entity. +/// +/// # Safety +/// +/// - The returned `EntityCommand` must be queued for the world where `component_id` was created. +/// - `T` must be the type represented by `component_id`. +unsafe fn insert_by_id( + component_id: ComponentId, + value: T, + on_none_entity: impl FnOnce(Entity) + Send + 'static, +) -> impl EntityCommand { move |entity, world: &mut World| { if let Some(mut entity) = world.get_entity_mut(entity) { - entity.insert(bundle); + // SAFETY: + // - `component_id` safety is ensured by the caller + // - `ptr` is valid within the `make` block; + OwningPtr::make(value, |ptr| unsafe { + entity.insert_by_id(component_id, ptr); + }); + } else { + on_none_entity(entity); } } } @@ -1290,19 +1458,28 @@ fn retain(entity: Entity, world: &mut World) { /// A [`Command`] that inserts a [`Resource`] into the world using a value /// created with the [`FromWorld`] trait. +#[track_caller] fn init_resource(world: &mut World) { world.init_resource::(); } /// A [`Command`] that removes the [resource](Resource) `R` from the world. +#[track_caller] fn remove_resource(world: &mut World) { world.remove_resource::(); } /// A [`Command`] that inserts a [`Resource`] into the world. +#[track_caller] fn insert_resource(resource: R) -> impl Command { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); move |world: &mut World| { - world.insert_resource(resource); + world.insert_resource_with_caller( + resource, + #[cfg(feature = "track_change_detection")] + caller, + ); } } @@ -1543,6 +1720,15 @@ mod tests { assert!(world.contains_resource::>()); } + fn is_send() {} + fn is_sync() {} + + #[test] + fn test_commands_are_send_and_sync() { + is_send::(); + is_sync::(); + } + #[test] fn append() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 3d54e882b5d594..08b4b678bd5262 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -30,6 +30,19 @@ where marker: PhantomData Marker>, } +impl ExclusiveFunctionSystem +where + F: ExclusiveSystemParamFunction, +{ + /// Return this system with a new name. + /// + /// Useful to give closure systems more readable and unique names for debugging and tracing. + pub fn with_name(mut self, new_name: impl Into>) -> Self { + self.system_meta.set_name(new_name.into()); + self + } +} + /// A marker type used to distinguish exclusive function systems from regular function systems. #[doc(hidden)] pub struct IsExclusiveFunctionSystem; @@ -111,9 +124,7 @@ where let out = self.func.run(world, input, params); world.flush(); - let change_tick = world.change_tick.get_mut(); - self.system_meta.last_run.set(*change_tick); - *change_tick = change_tick.wrapping_add(1); + self.system_meta.last_run = world.increment_change_tick(); out }) diff --git a/crates/bevy_ecs/src/system/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index 93ad2e603a247b..255da95fec6826 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -87,9 +87,10 @@ impl ExclusiveSystemParam for PhantomData { } macro_rules! impl_exclusive_system_param_tuple { - ($($param: ident),*) => { + ($(#[$meta:meta])* $($param: ident),*) => { #[allow(unused_variables)] #[allow(non_snake_case)] + $(#[$meta])* impl<$($param: ExclusiveSystemParam),*> ExclusiveSystemParam for ($($param,)*) { type State = ($($param::State,)*); type Item<'s> = ($($param::Item<'s>,)*); @@ -113,7 +114,13 @@ macro_rules! impl_exclusive_system_param_tuple { }; } -all_tuples!(impl_exclusive_system_param_tuple, 0, 16, P); +all_tuples!( + #[doc(fake_variadic)] + impl_exclusive_system_param_tuple, + 0, + 16, + P +); #[cfg(test)] mod tests { diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index bf2b2c4fd5dbc3..fe2420a663b85a 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -14,13 +14,25 @@ use std::{borrow::Cow, marker::PhantomData}; #[cfg(feature = "trace")] use bevy_utils::tracing::{info_span, Span}; -use super::{In, IntoSystem, ReadOnlySystem, SystemBuilder}; +use super::{In, IntoSystem, ReadOnlySystem, SystemParamBuilder}; /// The metadata of a [`System`]. #[derive(Clone)] pub struct SystemMeta { pub(crate) name: Cow<'static, str>, + /// The set of component accesses for this system. This is used to determine + /// - soundness issues (e.g. multiple [`SystemParam`]s mutably accessing the same component) + /// - ambiguities in the schedule (e.g. two systems that have some sort of conflicting access) pub(crate) component_access_set: FilteredAccessSet, + /// This [`Access`] is used to determine which systems can run in parallel with each other + /// in the multithreaded executor. + /// + /// We use a [`ArchetypeComponentId`] as it is more precise than just checking [`ComponentId`]: + /// for example if you have one system with `Query<&mut T, With>` and one system with `Query<&mut T, With>` + /// they conflict if you just look at the [`ComponentId`] of `T`; but if there are no archetypes with + /// both `A`, `B` and `T` then in practice there's no risk of conflict. By using [`ArchetypeComponentId`] + /// we can be more precise because we can check if the existing archetypes of the [`World`] + /// cause a conflict pub(crate) archetype_component_access: Access, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other @@ -56,6 +68,20 @@ impl SystemMeta { &self.name } + /// Sets the name of of this system. + /// + /// Useful to give closure systems more readable and unique names for debugging and tracing. + pub fn set_name(&mut self, new_name: impl Into>) { + let new_name: Cow<'static, str> = new_name.into(); + #[cfg(feature = "trace")] + { + let name = new_name.as_ref(); + self.system_span = info_span!("system", name = name); + self.commands_span = info_span!("system_commands", name = name); + } + self.name = new_name; + } + /// Returns true if the system is [`Send`]. #[inline] pub fn is_send(&self) -> bool { @@ -202,16 +228,34 @@ impl SystemState { } } - // Create a [`SystemState`] from a [`SystemBuilder`] - pub(crate) fn from_builder(builder: SystemBuilder) -> Self { + /// Create a [`SystemState`] from a [`SystemParamBuilder`] + pub(crate) fn from_builder(world: &mut World, builder: impl SystemParamBuilder) -> Self { + let mut meta = SystemMeta::new::(); + meta.last_run = world.change_tick().relative_to(Tick::MAX); + let param_state = builder.build(world, &mut meta); Self { - meta: builder.meta, - param_state: builder.state, - world_id: builder.world.id(), + meta, + param_state, + world_id: world.id(), archetype_generation: ArchetypeGeneration::initial(), } } + /// Create a [`FunctionSystem`] from a [`SystemState`]. + pub fn build_system>( + self, + func: F, + ) -> FunctionSystem { + FunctionSystem { + func, + param_state: Some(self.param_state), + system_meta: self.meta, + world_id: Some(self.world_id), + archetype_generation: self.archetype_generation, + marker: PhantomData, + } + } + /// Gets the metadata for this instance. #[inline] pub fn meta(&self) -> &SystemMeta { @@ -411,16 +455,12 @@ impl FunctionSystem where F: SystemParamFunction, { - // Create a [`FunctionSystem`] from a [`SystemBuilder`] - pub(crate) fn from_builder(builder: SystemBuilder, func: F) -> Self { - Self { - func, - param_state: Some(builder.state), - system_meta: builder.meta, - world_id: Some(builder.world.id()), - archetype_generation: ArchetypeGeneration::initial(), - marker: PhantomData, - } + /// Return this system with a new name. + /// + /// Useful to give closure systems more readable and unique names for debugging and tracing. + pub fn with_name(mut self, new_name: impl Into>) -> Self { + self.system_meta.set_name(new_name.into()); + self } } diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index d2cf03a96eda06..bfb2dcd396cceb 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -312,9 +312,9 @@ where assert_is_system(system); } -/// Ensures that the provided system doesn't with itself. +/// Ensures that the provided system doesn't conflict with itself. /// -/// This function will panic if the provided system conflict with itself. +/// This function will panic if the provided system conflict with itself. /// /// Note: this will run the system on an empty world. pub fn assert_system_does_not_conflict>(sys: S) { @@ -340,10 +340,11 @@ impl std::ops::DerefMut for In { #[cfg(test)] mod tests { - use std::any::TypeId; - use bevy_utils::default; + use std::any::TypeId; + use crate::prelude::EntityRef; + use crate::world::EntityMut; use crate::{ self as bevy_ecs, archetype::{ArchetypeComponentId, Archetypes}, @@ -808,6 +809,15 @@ mod tests { run_system(&mut world, sys); } + #[test] + #[should_panic] + fn changed_trackers_or_conflict() { + fn sys(_: Query<&mut A>, _: Query<(), Or<(Changed,)>>) {} + + let mut world = World::default(); + run_system(&mut world, sys); + } + #[test] fn query_set_system() { fn sys(mut _set: ParamSet<(Query<&mut A>, Query<&A>)>) {} @@ -1093,7 +1103,7 @@ mod tests { .get_resource_id(TypeId::of::()) .unwrap(); let d_id = world.components().get_id(TypeId::of::()).unwrap(); - assert_eq!(conflicts, vec![b_id, d_id]); + assert_eq!(conflicts, vec![b_id, d_id].into()); } #[test] @@ -1486,7 +1496,7 @@ mod tests { assert_eq!( system .archetype_component_access() - .reads() + .component_reads() .collect::>(), expected_ids ); @@ -1516,7 +1526,7 @@ mod tests { assert_eq!( system .archetype_component_access() - .reads() + .component_reads() .collect::>(), expected_ids ); @@ -1534,7 +1544,7 @@ mod tests { assert_eq!( system .archetype_component_access() - .reads() + .component_reads() .collect::>(), expected_ids ); @@ -1599,6 +1609,33 @@ mod tests { super::assert_system_does_not_conflict(system); } + #[test] + #[should_panic( + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + )] + fn assert_world_and_entity_mut_system_does_conflict() { + fn system(_query: &World, _q2: Query) {} + super::assert_system_does_not_conflict(system); + } + + #[test] + #[should_panic( + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + )] + fn assert_entity_ref_and_entity_mut_system_does_conflict() { + fn system(_query: Query, _q2: Query) {} + super::assert_system_does_not_conflict(system); + } + + #[test] + #[should_panic( + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + )] + fn assert_entity_mut_system_does_conflict() { + fn system(_query: Query, _q2: Query) {} + super::assert_system_does_not_conflict(system); + } + #[test] #[should_panic] fn panic_inside_system() { diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index fdd3f36dd5058a..c5a04f25dd4eb6 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -10,31 +10,40 @@ use super::IntoSystem; /// Implemented for systems that have an [`Observer`] as the first argument. /// /// [`Observer`]: crate::observer::Observer -pub trait ObserverSystem: - System, Out = ()> + Send + 'static +pub trait ObserverSystem: + System, Out = Out> + Send + 'static { } -impl, Out = ()> + Send + 'static> - ObserverSystem for T +impl< + E: 'static, + B: Bundle, + Out, + T: System, Out = Out> + Send + 'static, + > ObserverSystem for T { } /// Implemented for systems that convert into [`ObserverSystem`]. -pub trait IntoObserverSystem: Send + 'static { +pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. - type System: ObserverSystem; + type System: ObserverSystem; /// Turns this value into its corresponding [`System`]. fn into_system(this: Self) -> Self::System; } -impl, (), M> + Send + 'static, M, E: 'static, B: Bundle> - IntoObserverSystem for S +impl< + S: IntoSystem, Out, M> + Send + 'static, + M, + Out, + E: 'static, + B: Bundle, + > IntoObserverSystem for S where - S::System: ObserverSystem, + S::System: ObserverSystem, { - type System = , (), M>>::System; + type System = , Out, M>>::System; fn into_system(this: Self) -> Self::System { IntoSystem::into_system(this) @@ -44,23 +53,23 @@ where macro_rules! impl_system_function { ($($param: ident),*) => { #[allow(non_snake_case)] - impl SystemParamFunction, $($param,)*)> for Func + impl SystemParamFunction, $($param,)*)> for Func where for <'a> &'a mut Func: - FnMut(Trigger, $($param),*) + - FnMut(Trigger, $(SystemParamItem<$param>),*) + FnMut(Trigger, $($param),*) -> Out + + FnMut(Trigger, $(SystemParamItem<$param>),*) -> Out, Out: 'static { type In = Trigger<'static, E, B>; - type Out = (); + type Out = Out; type Param = ($($param,)*); #[inline] - fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) { + fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) -> Out { #[allow(clippy::too_many_arguments)] - fn call_inner( - mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*), + fn call_inner( + mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*) -> Out, input: Trigger<'static, E, B>, $($param: $param,)* - ){ + ) -> Out{ f(input, $($param,)*) } let ($($param,)*) = param_value; @@ -71,3 +80,37 @@ macro_rules! impl_system_function { } all_tuples!(impl_system_function, 0, 16, F); + +#[cfg(test)] +mod tests { + use crate::{ + self as bevy_ecs, + event::Event, + observer::Trigger, + system::{In, IntoSystem}, + world::World, + }; + + #[derive(Event)] + struct TriggerEvent; + + #[test] + fn test_piped_observer_systems_no_input() { + fn a(_: Trigger) {} + fn b() {} + + let mut world = World::new(); + world.observe(a.pipe(b)); + } + + #[test] + fn test_piped_observer_systems_with_inputs() { + fn a(_: Trigger) -> u32 { + 3 + } + fn b(_: In) {} + + let mut world = World::new(); + world.observe(a.pipe(b)); + } +} diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 215c8d7b1108bf..ffe595b2cfd015 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -405,6 +405,36 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { unsafe { Query::new(self.world, new_state, self.last_run, self.this_run) } } + /// Returns a new `Query` reborrowing the access from this one. The current query will be unusable + /// while the new one exists. + /// + /// # Example + /// + /// For example this allows to call other methods or other systems that require an owned `Query` without + /// completely giving up ownership of it. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct ComponentA; + /// + /// fn helper_system(query: Query<&ComponentA>) { /* ... */} + /// + /// fn system(mut query: Query<&ComponentA>) { + /// helper_system(query.reborrow()); + /// // Can still use query here: + /// for component in &query { + /// // ... + /// } + /// } + /// ``` + pub fn reborrow(&mut self) -> Query<'_, 's, D, F> { + // SAFETY: this query is exclusively borrowed while the new one exists, so + // no overlapping access can occur. + unsafe { Query::new(self.world, self.state, self.last_run, self.this_run) } + } + /// Returns an [`Iterator`] over the read-only query items. /// /// This iterator is always guaranteed to return results from each matching entity once and only once. @@ -1368,8 +1398,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn transmute_lens_filtered( &mut self, ) -> QueryLens<'_, NewD, NewF> { - let components = self.world.components(); - let state = self.state.transmute_filtered::(components); + let state = self.state.transmute_filtered::(self.world); QueryLens { world: self.world, state, @@ -1460,10 +1489,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { &mut self, other: &mut Query, ) -> QueryLens<'_, NewD, NewF> { - let components = self.world.components(); let state = self .state - .join_filtered::(components, other.state); + .join_filtered::(self.world, other.state); QueryLens { world: self.world, state, diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index cc61542f795a01..a158a8327cc14a 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -105,7 +105,7 @@ impl ExclusiveSystemParam for SystemName<'_> { #[cfg(test)] mod tests { - use crate::system::SystemName; + use crate::system::{IntoSystem, RunSystemOnce, SystemName}; use crate::world::World; #[test] @@ -131,4 +131,23 @@ mod tests { let name = world.run_system(id).unwrap(); assert!(name.ends_with("testing")); } + + #[test] + fn test_closure_system_name_regular_param() { + let mut world = World::default(); + let system = + IntoSystem::into_system(|name: SystemName| name.name().to_owned()).with_name("testing"); + let name = world.run_system_once(system); + assert_eq!(name, "testing"); + } + + #[test] + fn test_exclusive_closure_system_name_regular_param() { + let mut world = World::default(); + let system = + IntoSystem::into_system(|_world: &mut World, name: SystemName| name.name().to_owned()) + .with_name("testing"); + let name = world.run_system_once(system); + assert_eq!(name, "testing"); + } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 137bbfa696204b..3bf032590aa8d4 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -1,11 +1,12 @@ pub use crate::change_detection::{NonSendMut, Res, ResMut}; +use crate::query::AccessConflicts; +use crate::storage::SparseSetIndex; use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundles, change_detection::{Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Tick}, entity::Entities, - prelude::QueryBuilder, query::{ Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QueryState, ReadOnlyQueryData, @@ -18,6 +19,8 @@ pub use bevy_ecs_macros::Resource; pub use bevy_ecs_macros::SystemParam; use bevy_ptr::UnsafeCellDeref; use bevy_utils::{all_tuples, synccell::SyncCell}; +#[cfg(feature = "track_change_detection")] +use std::panic::Location; use std::{ fmt::Debug, marker::PhantomData, @@ -181,19 +184,6 @@ pub unsafe trait SystemParam: Sized { ) -> Self::Item<'world, 'state>; } -/// A parameter that can be built with [`SystemBuilder`](crate::system::builder::SystemBuilder) -pub trait BuildableSystemParam: SystemParam { - /// A mutable reference to this type will be passed to the builder function - type Builder<'b>; - - /// Constructs [`SystemParam::State`] for `Self` using a given builder function - fn build( - world: &mut World, - meta: &mut SystemMeta, - func: impl FnOnce(&mut Self::Builder<'_>), - ) -> Self::State; -} - /// A [`SystemParam`] that only reads a given [`World`]. /// /// # Safety @@ -217,17 +207,7 @@ unsafe impl SystemParam for Qu fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let state = QueryState::new_with_access(world, &mut system_meta.archetype_component_access); - assert_component_access_compatibility( - &system_meta.name, - std::any::type_name::(), - std::any::type_name::(), - &system_meta.component_access_set, - &state.component_access, - world, - ); - system_meta - .component_access_set - .add(state.component_access.clone()); + init_query_param(world, system_meta, &state); state } @@ -253,33 +233,22 @@ unsafe impl SystemParam for Qu } } -impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> BuildableSystemParam - for Query<'w, 's, D, F> -{ - type Builder<'b> = QueryBuilder<'b, D, F>; - - #[inline] - fn build( - world: &mut World, - system_meta: &mut SystemMeta, - build: impl FnOnce(&mut Self::Builder<'_>), - ) -> Self::State { - let mut builder = QueryBuilder::new(world); - build(&mut builder); - let state = builder.build(); - assert_component_access_compatibility( - &system_meta.name, - std::any::type_name::(), - std::any::type_name::(), - &system_meta.component_access_set, - &state.component_access, - world, - ); - system_meta - .component_access_set - .add(state.component_access.clone()); - state - } +pub(crate) fn init_query_param( + world: &mut World, + system_meta: &mut SystemMeta, + state: &QueryState, +) { + assert_component_access_compatibility( + &system_meta.name, + std::any::type_name::(), + std::any::type_name::(), + &system_meta.component_access_set, + &state.component_access, + world, + ); + system_meta + .component_access_set + .add(state.component_access.clone()); } fn assert_component_access_compatibility( @@ -294,12 +263,22 @@ fn assert_component_access_compatibility( if conflicts.is_empty() { return; } - let conflicting_components = conflicts - .into_iter() - .map(|component_id| world.components.get_info(component_id).unwrap().name()) - .collect::>(); - let accesses = conflicting_components.join(", "); - panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s) {accesses} in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"); + let accesses = match conflicts { + AccessConflicts::All => "", + AccessConflicts::Individual(indices) => &format!( + " {}", + indices + .ones() + .map(|index| world + .components + .get_info(ComponentId::get_sparse_set_index(index)) + .unwrap() + .name()) + .collect::>() + .join(", ") + ), + }; + panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s){accesses} in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"); } /// A collection of potentially conflicting [`SystemParam`]s allowed by disjoint access. @@ -404,7 +383,7 @@ fn assert_component_access_compatibility( /// # let _event = event; /// } /// set.p1().send(MyEvent::new()); -/// +/// /// let entities = set.p2().entities(); /// // ... /// # let _entities = entities; @@ -498,25 +477,22 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let component_id = world.components.init_resource::(); - world.initialize_resource_internal(component_id); + let archetype_component_id = world.initialize_resource_internal(component_id).id(); let combined_access = system_meta.component_access_set.combined_access(); assert!( - !combined_access.has_write(component_id), + !combined_access.has_resource_write(component_id), "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", std::any::type_name::(), system_meta.name, ); system_meta .component_access_set - .add_unfiltered_read(component_id); + .add_unfiltered_resource_read(component_id); - let archetype_component_id = world - .get_resource_archetype_component_id(component_id) - .unwrap(); system_meta .archetype_component_access - .add_read(archetype_component_id); + .add_resource_read(archetype_component_id); component_id } @@ -528,15 +504,16 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { world: UnsafeWorldCell<'w>, change_tick: Tick, ) -> Self::Item<'w, 's> { - let (ptr, ticks) = world - .get_resource_with_ticks(component_id) - .unwrap_or_else(|| { - panic!( - "Resource requested by {} does not exist: {}", - system_meta.name, - std::any::type_name::() - ) - }); + let (ptr, ticks, _caller) = + world + .get_resource_with_ticks(component_id) + .unwrap_or_else(|| { + panic!( + "Resource requested by {} does not exist: {}", + system_meta.name, + std::any::type_name::() + ) + }); Res { value: ptr.deref(), ticks: Ticks { @@ -545,6 +522,8 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { last_run: system_meta.last_run, this_run: change_tick, }, + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref(), } } } @@ -570,7 +549,7 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { ) -> Self::Item<'w, 's> { world .get_resource_with_ticks(component_id) - .map(|(ptr, ticks)| Res { + .map(|(ptr, ticks, _caller)| Res { value: ptr.deref(), ticks: Ticks { added: ticks.added.deref(), @@ -578,6 +557,8 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { last_run: system_meta.last_run, this_run: change_tick, }, + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref(), }) } } @@ -590,28 +571,25 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let component_id = world.components.init_resource::(); - world.initialize_resource_internal(component_id); + let archetype_component_id = world.initialize_resource_internal(component_id).id(); let combined_access = system_meta.component_access_set.combined_access(); - if combined_access.has_write(component_id) { + if combined_access.has_resource_write(component_id) { panic!( "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", std::any::type_name::(), system_meta.name); - } else if combined_access.has_read(component_id) { + } else if combined_access.has_resource_read(component_id) { panic!( "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", std::any::type_name::(), system_meta.name); } system_meta .component_access_set - .add_unfiltered_write(component_id); + .add_unfiltered_resource_write(component_id); - let archetype_component_id = world - .get_resource_archetype_component_id(component_id) - .unwrap(); system_meta .archetype_component_access - .add_write(archetype_component_id); + .add_resource_write(archetype_component_id); component_id } @@ -640,6 +618,8 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { last_run: system_meta.last_run, this_run: change_tick, }, + #[cfg(feature = "track_change_detection")] + changed_by: value.changed_by, } } } @@ -670,6 +650,8 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { last_run: system_meta.last_run, this_run: change_tick, }, + #[cfg(feature = "track_change_detection")] + changed_by: value.changed_by, }) } } @@ -851,20 +833,6 @@ unsafe impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { } } -impl<'w, T: FromWorld + Send + 'static> BuildableSystemParam for Local<'w, T> { - type Builder<'b> = T; - - fn build( - world: &mut World, - _meta: &mut SystemMeta, - func: impl FnOnce(&mut Self::Builder<'_>), - ) -> Self::State { - let mut value = T::from_world(world); - func(&mut value); - SyncCell::new(value) - } -} - /// Types that can be used with [`Deferred`] in systems. /// This allows storing system-local data which is used to defer [`World`] mutations. /// @@ -1071,6 +1039,8 @@ pub struct NonSend<'w, T: 'static> { ticks: ComponentTicks, last_run: Tick, this_run: Tick, + #[cfg(feature = "track_change_detection")] + changed_by: &'static Location<'static>, } // SAFETY: Only reads a single World non-send resource @@ -1095,6 +1065,12 @@ impl<'w, T: 'static> NonSend<'w, T> { pub fn is_changed(&self) -> bool { self.ticks.is_changed(self.last_run, self.this_run) } + + /// The location that last caused this to change. + #[cfg(feature = "track_change_detection")] + pub fn changed_by(&self) -> &'static Location<'static> { + self.changed_by + } } impl<'w, T> Deref for NonSend<'w, T> { @@ -1114,6 +1090,8 @@ impl<'a, T> From> for NonSend<'a, T> { }, this_run: nsm.ticks.this_run, last_run: nsm.ticks.last_run, + #[cfg(feature = "track_change_detection")] + changed_by: nsm.changed_by, } } } @@ -1128,25 +1106,22 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { system_meta.set_non_send(); let component_id = world.components.init_non_send::(); - world.initialize_non_send_internal(component_id); + let archetype_component_id = world.initialize_non_send_internal(component_id).id(); let combined_access = system_meta.component_access_set.combined_access(); assert!( - !combined_access.has_write(component_id), + !combined_access.has_resource_write(component_id), "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", std::any::type_name::(), system_meta.name, ); system_meta .component_access_set - .add_unfiltered_read(component_id); + .add_unfiltered_resource_read(component_id); - let archetype_component_id = world - .get_non_send_archetype_component_id(component_id) - .unwrap(); system_meta .archetype_component_access - .add_read(archetype_component_id); + .add_resource_read(archetype_component_id); component_id } @@ -1158,21 +1133,24 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { world: UnsafeWorldCell<'w>, change_tick: Tick, ) -> Self::Item<'w, 's> { - let (ptr, ticks) = world - .get_non_send_with_ticks(component_id) - .unwrap_or_else(|| { - panic!( - "Non-send resource requested by {} does not exist: {}", - system_meta.name, - std::any::type_name::() - ) - }); + let (ptr, ticks, _caller) = + world + .get_non_send_with_ticks(component_id) + .unwrap_or_else(|| { + panic!( + "Non-send resource requested by {} does not exist: {}", + system_meta.name, + std::any::type_name::() + ) + }); NonSend { value: ptr.deref(), ticks: ticks.read(), last_run: system_meta.last_run, this_run: change_tick, + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref(), } } } @@ -1198,11 +1176,13 @@ unsafe impl SystemParam for Option> { ) -> Self::Item<'w, 's> { world .get_non_send_with_ticks(component_id) - .map(|(ptr, ticks)| NonSend { + .map(|(ptr, ticks, _caller)| NonSend { value: ptr.deref(), ticks: ticks.read(), last_run: system_meta.last_run, this_run: change_tick, + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref(), }) } } @@ -1217,28 +1197,25 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { system_meta.set_non_send(); let component_id = world.components.init_non_send::(); - world.initialize_non_send_internal(component_id); + let archetype_component_id = world.initialize_non_send_internal(component_id).id(); let combined_access = system_meta.component_access_set.combined_access(); - if combined_access.has_write(component_id) { + if combined_access.has_component_write(component_id) { panic!( "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", std::any::type_name::(), system_meta.name); - } else if combined_access.has_read(component_id) { + } else if combined_access.has_component_read(component_id) { panic!( "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", std::any::type_name::(), system_meta.name); } system_meta .component_access_set - .add_unfiltered_write(component_id); + .add_unfiltered_resource_write(component_id); - let archetype_component_id = world - .get_non_send_archetype_component_id(component_id) - .unwrap(); system_meta .archetype_component_access - .add_write(archetype_component_id); + .add_resource_write(archetype_component_id); component_id } @@ -1250,18 +1227,21 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { world: UnsafeWorldCell<'w>, change_tick: Tick, ) -> Self::Item<'w, 's> { - let (ptr, ticks) = world - .get_non_send_with_ticks(component_id) - .unwrap_or_else(|| { - panic!( - "Non-send resource requested by {} does not exist: {}", - system_meta.name, - std::any::type_name::() - ) - }); + let (ptr, ticks, _caller) = + world + .get_non_send_with_ticks(component_id) + .unwrap_or_else(|| { + panic!( + "Non-send resource requested by {} does not exist: {}", + system_meta.name, + std::any::type_name::() + ) + }); NonSendMut { value: ptr.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref_mut(), } } } @@ -1284,9 +1264,11 @@ unsafe impl<'a, T: 'static> SystemParam for Option> { ) -> Self::Item<'w, 's> { world .get_non_send_with_ticks(component_id) - .map(|(ptr, ticks)| NonSendMut { + .map(|(ptr, ticks, _caller)| NonSendMut { value: ptr.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref_mut(), }) } } @@ -1428,13 +1410,15 @@ unsafe impl SystemParam for SystemChangeTick { } macro_rules! impl_system_param_tuple { - ($($param: ident),*) => { + ($(#[$meta:meta])* $($param: ident),*) => { + $(#[$meta])* // SAFETY: tuple consists only of ReadOnlySystemParams unsafe impl<$($param: ReadOnlySystemParam),*> ReadOnlySystemParam for ($($param,)*) {} // SAFETY: implementors of each `SystemParam` in the tuple have validated their impls #[allow(clippy::undocumented_unsafe_blocks)] // false positive by clippy #[allow(non_snake_case)] + $(#[$meta])* unsafe impl<$($param: SystemParam),*> SystemParam for ($($param,)*) { type State = ($($param::State,)*); type Item<'w, 's> = ($($param::Item::<'w, 's>,)*); @@ -1477,7 +1461,13 @@ macro_rules! impl_system_param_tuple { }; } -all_tuples!(impl_system_param_tuple, 0, 16, P); +all_tuples!( + #[doc(fake_variadic)] + impl_system_param_tuple, + 0, + 16, + P +); /// Contains type aliases for built-in [`SystemParam`]s with `'static` lifetimes. /// This makes it more convenient to refer to these types in contexts where diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 96b1ea9471ab05..f6cf792f71a85c 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -12,6 +12,10 @@ struct RegisteredSystem { system: BoxedSystem, } +/// Marker [`Component`](bevy_ecs::component::Component) for identifying [`SystemId`] [`Entity`]s. +#[derive(Component)] +pub struct SystemIdMarker; + /// A system that has been removed from the registry. /// It contains the system and whether or not it has been initialized. /// @@ -94,10 +98,7 @@ impl std::hash::Hash for SystemId { impl std::fmt::Debug for SystemId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("SystemId") - .field(&self.entity) - .field(&self.entity) - .finish() + f.debug_tuple("SystemId").field(&self.entity).finish() } } @@ -128,10 +129,13 @@ impl World { ) -> SystemId { SystemId { entity: self - .spawn(RegisteredSystem { - initialized: false, - system, - }) + .spawn(( + RegisteredSystem { + initialized: false, + system, + }, + SystemIdMarker, + )) .id(), marker: std::marker::PhantomData, } diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs new file mode 100644 index 00000000000000..7e4705f1eb0979 --- /dev/null +++ b/crates/bevy_ecs/src/traversal.rs @@ -0,0 +1,43 @@ +//! A trait for components that let you traverse the ECS. + +use crate::{ + component::{Component, StorageType}, + entity::Entity, +}; + +/// A component that can point to another entity, and which can be used to define a path through the ECS. +/// +/// Traversals are used to [specify the direction] of [event propagation] in [observers]. By default, +/// events use the [`TraverseNone`] placeholder component, which cannot actually be created or added to +/// an entity and so never causes traversal. +/// +/// Infinite loops are possible, and are not checked for. While looping can be desirable in some contexts +/// (for example, an observer that triggers itself multiple times before stopping), following an infinite +/// traversal loop without an eventual exit will can your application to hang. Each implementer of `Traversal` +/// for documenting possible looping behavior, and consumers of those implementations are responsible for +/// avoiding infinite loops in their code. +/// +/// [specify the direction]: crate::event::Event::Traversal +/// [event propagation]: crate::observer::Trigger::propagate +/// [observers]: crate::observer::Observer +pub trait Traversal: Component { + /// Returns the next entity to visit. + fn traverse(&self) -> Option; +} + +/// A traversal component that doesn't traverse anything. Used to provide a default traversal +/// implementation for events. +/// +/// It is not possible to actually construct an instance of this component. +pub enum TraverseNone {} + +impl Traversal for TraverseNone { + #[inline(always)] + fn traverse(&self) -> Option { + None + } +} + +impl Component for TraverseNone { + const STORAGE_TYPE: StorageType = StorageType::Table; +} diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index a682443a4670a7..e2a0a29f405592 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -2,7 +2,7 @@ use crate::system::{SystemBuffer, SystemMeta}; use std::{ fmt::Debug, - mem::MaybeUninit, + mem::{size_of, MaybeUninit}, panic::{self, AssertUnwindSafe}, ptr::{addr_of_mut, NonNull}, }; @@ -169,7 +169,7 @@ impl RawCommandQueue { let meta = CommandMeta { consume_command_and_get_size: |command, world, cursor| { - *cursor += std::mem::size_of::(); + *cursor += size_of::(); // SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`, // `command` must point to a value of type `C`. @@ -197,7 +197,7 @@ impl RawCommandQueue { let old_len = bytes.len(); // Reserve enough bytes for both the metadata and the command itself. - bytes.reserve(std::mem::size_of::>()); + bytes.reserve(size_of::>()); // Pointer to the bytes at the end of the buffer. // SAFETY: We know it is within bounds of the allocation, due to the call to `.reserve()`. @@ -217,7 +217,7 @@ impl RawCommandQueue { // SAFETY: The new length is guaranteed to fit in the vector's capacity, // due to the call to `.reserve()` above. unsafe { - bytes.set_len(old_len + std::mem::size_of::>()); + bytes.set_len(old_len + size_of::>()); } } @@ -252,13 +252,13 @@ impl RawCommandQueue { }; // Advance to the bytes just after `meta`, which represent a type-erased command. - local_cursor += std::mem::size_of::(); + local_cursor += size_of::(); // Construct an owned pointer to the command. // SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above // guarantees that nothing stored in the buffer will get observed after this function ends. // `cmd` points to a valid address of a stored command, so it must be non-null. let cmd = unsafe { - OwningPtr::::new(std::ptr::NonNull::new_unchecked( + OwningPtr::::new(NonNull::new_unchecked( self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(), )) }; @@ -445,7 +445,7 @@ mod test { #[test] fn test_command_queue_inner_panic_safe() { - std::panic::set_hook(Box::new(|_| {})); + panic::set_hook(Box::new(|_| {})); let mut queue = CommandQueue::default(); @@ -454,7 +454,7 @@ mod test { let mut world = World::new(); - let _ = std::panic::catch_unwind(AssertUnwindSafe(|| { + let _ = panic::catch_unwind(AssertUnwindSafe(|| { queue.apply(&mut world); })); @@ -468,7 +468,7 @@ mod test { #[test] fn test_command_queue_inner_nested_panic_safe() { - std::panic::set_hook(Box::new(|_| {})); + panic::set_hook(Box::new(|_| {})); #[derive(Resource, Default)] struct Order(Vec); @@ -488,7 +488,7 @@ mod test { }); world.commands().add(add_index(4)); - let _ = std::panic::catch_unwind(AssertUnwindSafe(|| { + let _ = panic::catch_unwind(AssertUnwindSafe(|| { world.flush_commands(); })); diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs index 00dee013081c40..92c1572b0935bb 100644 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ b/crates/bevy_ecs/src/world/component_constants.rs @@ -1,5 +1,7 @@ use super::*; use crate::{self as bevy_ecs}; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; /// Internal components used by bevy with a fixed component id. /// Constants are used to skip [`TypeId`] lookups in hot paths. @@ -7,17 +9,31 @@ use crate::{self as bevy_ecs}; pub const ON_ADD: ComponentId = ComponentId::new(0); /// [`ComponentId`] for [`OnInsert`] pub const ON_INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`OnReplace`] +pub const ON_REPLACE: ComponentId = ComponentId::new(2); /// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(2); +pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// Trigger emitted when a component is added to an entity. +/// Trigger emitted when a component is added to an entity. See [`crate::component::ComponentHooks::on_add`] +/// for more information. #[derive(Event)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct OnAdd; -/// Trigger emitted when a component is inserted onto an entity. +/// Trigger emitted when a component is inserted onto an entity. See [`crate::component::ComponentHooks::on_insert`] +/// for more information. #[derive(Event)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct OnInsert; -/// Trigger emitted when a component is removed from an entity. +/// Trigger emitted when a component is replaced on an entity. See [`crate::component::ComponentHooks::on_replace`] +/// for more information. #[derive(Event)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct OnReplace; + +/// Trigger emitted when a component is removed from an entity. See [`crate::component::ComponentHooks::on_remove`] +/// for more information. +#[derive(Event)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct OnRemove; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 9d0350abf268bf..90205b2ed43608 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -10,6 +10,7 @@ use crate::{ prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, system::{Commands, Query, Resource}, + traversal::Traversal, }; use super::{ @@ -118,9 +119,9 @@ impl<'w> DeferredWorld<'w> { /// If state is from a different world then self #[inline] pub fn query<'s, D: QueryData, F: QueryFilter>( - &'w mut self, + &mut self, state: &'s mut QueryState, - ) -> Query<'w, 's, D, F> { + ) -> Query<'_, 's, D, F> { state.validate_world(self.world.id()); state.update_archetypes(self); // SAFETY: We ran validate_world to ensure our state matches @@ -316,6 +317,28 @@ impl<'w> DeferredWorld<'w> { } } + /// Triggers all `on_replace` hooks for [`ComponentId`] in target. + /// + /// # Safety + /// Caller must ensure [`ComponentId`] in target exist in self. + #[inline] + pub(crate) unsafe fn trigger_on_replace( + &mut self, + archetype: &Archetype, + entity: Entity, + targets: impl Iterator, + ) { + if archetype.has_replace_hook() { + for component_id in targets { + // SAFETY: Caller ensures that these components exist + let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); + if let Some(hook) = hooks.on_replace { + hook(DeferredWorld { world: self.world }, entity, component_id); + } + } + } + } + /// Triggers all `on_remove` hooks for [`ComponentId`] in target. /// /// # Safety @@ -329,9 +352,8 @@ impl<'w> DeferredWorld<'w> { ) { if archetype.has_remove_hook() { for component_id in targets { - let hooks = // SAFETY: Caller ensures that these components exist - unsafe { self.world.components().get_info_unchecked(component_id) }.hooks(); + let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks(); if let Some(hook) = hooks.on_remove { hook(DeferredWorld { world: self.world }, entity, component_id); } @@ -348,9 +370,16 @@ impl<'w> DeferredWorld<'w> { &mut self, event: ComponentId, entity: Entity, - components: impl Iterator, + components: &[ComponentId], ) { - Observers::invoke(self.reborrow(), event, entity, components, &mut ()); + Observers::invoke::<_>( + self.reborrow(), + event, + entity, + components.iter().copied(), + &mut (), + &mut false, + ); } /// Triggers all event observers for [`ComponentId`] in target. @@ -358,14 +387,34 @@ impl<'w> DeferredWorld<'w> { /// # Safety /// Caller must ensure `E` is accessible as the type represented by `event` #[inline] - pub(crate) unsafe fn trigger_observers_with_data( + pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - entity: Entity, - components: impl Iterator, + mut entity: Entity, + components: &[ComponentId], data: &mut E, - ) { - Observers::invoke(self.reborrow(), event, entity, components, data); + mut propagate: bool, + ) where + C: Traversal, + { + loop { + Observers::invoke::<_>( + self.reborrow(), + event, + entity, + components.iter().copied(), + data, + &mut propagate, + ); + if !propagate { + break; + } + if let Some(traverse_to) = self.get::(entity).and_then(C::traverse) { + entity = traverse_to; + } else { + break; + } + } } /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. @@ -374,7 +423,11 @@ impl<'w> DeferredWorld<'w> { } /// Sends a [`Trigger`](crate::observer::Trigger) with the given `targets`. - pub fn trigger_targets(&mut self, trigger: impl Event, targets: impl TriggerTargets) { + pub fn trigger_targets( + &mut self, + trigger: impl Event, + targets: impl TriggerTargets + Send + Sync + 'static, + ) { self.commands().trigger_targets(trigger, targets); } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 0158b05f4b1b6f..0aa28241557e7b 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,6 +1,6 @@ use crate::{ archetype::{Archetype, ArchetypeId, Archetypes}, - bundle::{Bundle, BundleId, BundleInfo, BundleInserter, DynamicBundle}, + bundle::{Bundle, BundleId, BundleInfo, BundleInserter, DynamicBundle, InsertMode}, change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, @@ -16,7 +16,7 @@ use bevy_ptr::{OwningPtr, Ptr}; use std::{any::TypeId, marker::PhantomData}; use thiserror::Error; -use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE}; +use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE, ON_REPLACE}; /// A read-only reference to a particular [`Entity`] and all of its components. /// @@ -768,12 +768,47 @@ impl<'w> EntityWorldMut<'w> { /// Adds a [`Bundle`] of components to the entity. /// /// This will overwrite any previous value(s) of the same component type. + #[track_caller] pub fn insert(&mut self, bundle: T) -> &mut Self { + self.insert_with_caller( + bundle, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + core::panic::Location::caller(), + ) + } + + /// Adds a [`Bundle`] of components to the entity without overwriting. + /// + /// This will leave any previous value(s) of the same component type + /// unchanged. + #[track_caller] + pub fn insert_if_new(&mut self, bundle: T) -> &mut Self { + self.insert_with_caller( + bundle, + InsertMode::Keep, + #[cfg(feature = "track_change_detection")] + core::panic::Location::caller(), + ) + } + + /// Split into a new function so we can pass the calling location into the function when using + /// as a command. + #[inline] + pub(crate) fn insert_with_caller( + &mut self, + bundle: T, + mode: InsertMode, + #[cfg(feature = "track_change_detection")] caller: &'static core::panic::Location, + ) -> &mut Self { let change_tick = self.world.change_tick(); let mut bundle_inserter = BundleInserter::new::(self.world, self.location.archetype_id, change_tick); - // SAFETY: location matches current entity. `T` matches `bundle_info` - self.location = unsafe { bundle_inserter.insert(self.entity, self.location, bundle) }; + self.location = + // SAFETY: location matches current entity. `T` matches `bundle_info` + unsafe { + bundle_inserter.insert(self.entity, self.location, bundle, mode, #[cfg(feature = "track_change_detection")] caller) + }; self } @@ -787,6 +822,7 @@ impl<'w> EntityWorldMut<'w> { /// /// - [`ComponentId`] must be from the same world as [`EntityWorldMut`] /// - [`OwningPtr`] must be a valid reference to the type represented by [`ComponentId`] + #[track_caller] pub unsafe fn insert_by_id( &mut self, component_id: ComponentId, @@ -828,6 +864,7 @@ impl<'w> EntityWorldMut<'w> { /// # Safety /// - Each [`ComponentId`] must be from the same world as [`EntityWorldMut`] /// - Each [`OwningPtr`] must be a valid reference to the type represented by [`ComponentId`] + #[track_caller] pub unsafe fn insert_by_ids<'a, I: Iterator>>( &mut self, component_ids: &[ComponentId], @@ -904,7 +941,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: all bundle components exist in World unsafe { - trigger_on_remove_hooks_and_observers( + trigger_on_replace_and_on_remove_hooks_and_observers( &mut deferred_world, old_archetype, entity, @@ -1085,7 +1122,7 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: all bundle components exist in World unsafe { - trigger_on_remove_hooks_and_observers( + trigger_on_replace_and_on_remove_hooks_and_observers( &mut deferred_world, old_archetype, entity, @@ -1222,9 +1259,21 @@ impl<'w> EntityWorldMut<'w> { // SAFETY: All components in the archetype exist in world unsafe { + deferred_world.trigger_on_replace(archetype, self.entity, archetype.components()); + if archetype.has_replace_observer() { + deferred_world.trigger_observers( + ON_REPLACE, + self.entity, + &archetype.components().collect::>(), + ); + } deferred_world.trigger_on_remove(archetype, self.entity, archetype.components()); if archetype.has_remove_observer() { - deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components()); + deferred_world.trigger_observers( + ON_REMOVE, + self.entity, + &archetype.components().collect::>(), + ); } } @@ -1410,6 +1459,12 @@ impl<'w> EntityWorldMut<'w> { } } + /// Triggers the given `event` for this entity, which will run any observers watching for it. + pub fn trigger(&mut self, event: impl Event) -> &mut Self { + self.world.trigger_targets(event, self.entity); + self + } + /// Creates an [`Observer`] listening for events of type `E` targeting this entity. /// In order to trigger the callback the entity must also match the query when the event is fired. pub fn observe( @@ -1423,15 +1478,19 @@ impl<'w> EntityWorldMut<'w> { } /// SAFETY: all components in the archetype must exist in world -unsafe fn trigger_on_remove_hooks_and_observers( +unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers( deferred_world: &mut DeferredWorld, archetype: &Archetype, entity: Entity, bundle_info: &BundleInfo, ) { + deferred_world.trigger_on_replace(archetype, entity, bundle_info.iter_components()); + if archetype.has_replace_observer() { + deferred_world.trigger_observers(ON_REPLACE, entity, bundle_info.components()); + } deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_components()); if archetype.has_remove_observer() { - deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_components()); + deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.components()); } } @@ -1823,7 +1882,7 @@ impl<'w> FilteredEntityRef<'w> { /// Returns an iterator over the component ids that are accessed by self. #[inline] pub fn components(&self) -> impl Iterator + '_ { - self.access.reads_and_writes() + self.access.component_reads_and_writes() } /// Returns a reference to the underlying [`Access`]. @@ -1875,7 +1934,7 @@ impl<'w> FilteredEntityRef<'w> { pub fn get(&self) -> Option<&'w T> { let id = self.entity.world().components().get_id(TypeId::of::())?; self.access - .has_read(id) + .has_component_read(id) // SAFETY: We have read access .then(|| unsafe { self.entity.get() }) .flatten() @@ -1889,7 +1948,7 @@ impl<'w> FilteredEntityRef<'w> { pub fn get_ref(&self) -> Option> { let id = self.entity.world().components().get_id(TypeId::of::())?; self.access - .has_read(id) + .has_component_read(id) // SAFETY: We have read access .then(|| unsafe { self.entity.get_ref() }) .flatten() @@ -1901,7 +1960,7 @@ impl<'w> FilteredEntityRef<'w> { pub fn get_change_ticks(&self) -> Option { let id = self.entity.world().components().get_id(TypeId::of::())?; self.access - .has_read(id) + .has_component_read(id) // SAFETY: We have read access .then(|| unsafe { self.entity.get_change_ticks::() }) .flatten() @@ -1916,7 +1975,7 @@ impl<'w> FilteredEntityRef<'w> { #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { self.access - .has_read(component_id) + .has_component_read(component_id) // SAFETY: We have read access .then(|| unsafe { self.entity.get_change_ticks_by_id(component_id) }) .flatten() @@ -1933,7 +1992,7 @@ impl<'w> FilteredEntityRef<'w> { #[inline] pub fn get_by_id(&self, component_id: ComponentId) -> Option> { self.access - .has_read(component_id) + .has_component_read(component_id) // SAFETY: We have read access .then(|| unsafe { self.entity.get_by_id(component_id) }) .flatten() @@ -2080,7 +2139,7 @@ impl<'w> FilteredEntityMut<'w> { /// Returns an iterator over the component ids that are accessed by self. #[inline] pub fn components(&self) -> impl Iterator + '_ { - self.access.reads_and_writes() + self.access.component_reads_and_writes() } /// Returns a reference to the underlying [`Access`]. @@ -2148,7 +2207,7 @@ impl<'w> FilteredEntityMut<'w> { pub fn get_mut(&mut self) -> Option> { let id = self.entity.world().components().get_id(TypeId::of::())?; self.access - .has_write(id) + .has_component_write(id) // SAFETY: We have write access .then(|| unsafe { self.entity.get_mut() }) .flatten() @@ -2161,7 +2220,7 @@ impl<'w> FilteredEntityMut<'w> { pub fn into_mut(self) -> Option> { let id = self.entity.world().components().get_id(TypeId::of::())?; self.access - .has_write(id) + .has_component_write(id) // SAFETY: We have write access .then(|| unsafe { self.entity.get_mut() }) .flatten() @@ -2209,7 +2268,7 @@ impl<'w> FilteredEntityMut<'w> { #[inline] pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { self.access - .has_write(component_id) + .has_component_write(component_id) // SAFETY: We have write access .then(|| unsafe { self.entity.get_mut_by_id(component_id) }) .flatten() @@ -2284,6 +2343,7 @@ pub enum TryFromFilteredError { /// - [`OwningPtr`] and [`StorageType`] iterators must correspond to the /// [`BundleInfo`] used to construct [`BundleInserter`] /// - [`Entity`] must correspond to [`EntityLocation`] +#[track_caller] unsafe fn insert_dynamic_bundle< 'a, I: Iterator>, @@ -2312,7 +2372,16 @@ unsafe fn insert_dynamic_bundle< }; // SAFETY: location matches current entity. - unsafe { bundle_inserter.insert(entity, location, bundle) } + unsafe { + bundle_inserter.insert( + entity, + location, + bundle, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + core::panic::Location::caller(), + ) + } } /// Removes a bundle from the given archetype and returns the resulting archetype (or None if the diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index b075205818d691..bc22a96b595721 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -54,11 +54,11 @@ unsafe impl SystemParam for WorldId { type Item<'world, 'state> = WorldId; - fn init_state(_: &mut World, _: &mut crate::system::SystemMeta) -> Self::State {} + fn init_state(_: &mut World, _: &mut SystemMeta) -> Self::State {} unsafe fn get_param<'world, 'state>( _: &'state mut Self::State, - _: &crate::system::SystemMeta, + _: &SystemMeta, world: UnsafeWorldCell<'world>, _: Tick, ) -> Self::Item<'world, 'state> { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index bf4cf166727a35..6f1f1a12141780 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -9,6 +9,9 @@ mod identifier; mod spawn_batch; pub mod unsafe_world_cell; +#[cfg(feature = "bevy_reflect")] +pub mod reflect; + pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, @@ -23,8 +26,8 @@ pub use identifier::WorldId; pub use spawn_batch::*; use crate::{ - archetype::{ArchetypeComponentId, ArchetypeId, ArchetypeRow, Archetypes}, - bundle::{Bundle, BundleInfo, BundleInserter, BundleSpawner, Bundles}, + archetype::{ArchetypeId, ArchetypeRow, Archetypes}, + bundle::{Bundle, BundleInfo, BundleInserter, BundleSpawner, Bundles, InsertMode}, change_detection::{MutUntyped, TicksMut}, component::{ Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks, @@ -38,8 +41,7 @@ use crate::{ schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, system::{Commands, Res, Resource}, - world::command_queue::RawCommandQueue, - world::error::TryRunScheduleError, + world::{command_queue::RawCommandQueue, error::TryRunScheduleError}, }; use bevy_ptr::{OwningPtr, Ptr}; use bevy_utils::tracing::warn; @@ -49,6 +51,12 @@ use std::{ mem::MaybeUninit, sync::atomic::{AtomicU32, Ordering}, }; + +#[cfg(feature = "track_change_detection")] +use bevy_ptr::UnsafeCellDeref; + +use core::panic::Location; + use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; /// A [`World`] mutation. @@ -164,6 +172,7 @@ impl World { fn bootstrap(&mut self) { assert_eq!(ON_ADD, self.init_component::()); assert_eq!(ON_INSERT, self.init_component::()); + assert_eq!(ON_REPLACE, self.init_component::()); assert_eq!(ON_REMOVE, self.init_component::()); } /// Creates a new empty [`World`]. @@ -967,6 +976,7 @@ impl World { /// let position = world.entity(entity).get::().unwrap(); /// assert_eq!(position.x, 2.0); /// ``` + #[track_caller] pub fn spawn(&mut self, bundle: B) -> EntityWorldMut { self.flush(); let change_tick = self.change_tick(); @@ -974,7 +984,14 @@ impl World { let entity_location = { let mut bundle_spawner = BundleSpawner::new::(self, change_tick); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent - unsafe { bundle_spawner.spawn_non_existent(entity, bundle) } + unsafe { + bundle_spawner.spawn_non_existent( + entity, + bundle, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } }; // SAFETY: entity and location are valid, as they were just created above @@ -1019,12 +1036,18 @@ impl World { /// /// assert_eq!(entities.len(), 2); /// ``` + #[track_caller] pub fn spawn_batch(&mut self, iter: I) -> SpawnBatchIter<'_, I::IntoIter> where I: IntoIterator, I::Item: Bundle, { - SpawnBatchIter::new(self, iter.into_iter()) + SpawnBatchIter::new( + self, + iter.into_iter(), + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) } /// Retrieves a reference to the given `entity`'s [`Component`] of the given type. @@ -1097,14 +1120,24 @@ impl World { /// assert!(world.get_entity(entity).is_none()); /// assert!(world.get::(entity).is_none()); /// ``` + #[track_caller] #[inline] pub fn despawn(&mut self, entity: Entity) -> bool { + self.despawn_with_caller(entity, Location::caller()) + } + + #[inline] + pub(crate) fn despawn_with_caller( + &mut self, + entity: Entity, + caller: &'static Location, + ) -> bool { self.flush(); if let Some(entity) = self.get_entity_mut(entity) { entity.despawn(); true } else { - warn!("error[B0003]: Could not despawn entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", entity); + warn!("error[B0003]: {caller}: Could not despawn entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", entity); false } } @@ -1273,7 +1306,10 @@ impl World { /// Note that any resource with the [`Default`] trait automatically implements [`FromWorld`], /// and those default values will be here instead. #[inline] + #[track_caller] pub fn init_resource(&mut self) -> ComponentId { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); let component_id = self.components.init_resource::(); if self .storages @@ -1285,7 +1321,12 @@ impl World { OwningPtr::make(value, |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { - self.insert_resource_by_id(component_id, ptr); + self.insert_resource_by_id( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + caller, + ); } }); } @@ -1298,12 +1339,33 @@ impl World { /// If you insert a resource of a type that already exists, /// you will overwrite any existing data. #[inline] + #[track_caller] pub fn insert_resource(&mut self, value: R) { + self.insert_resource_with_caller( + value, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ); + } + + /// Split into a new function so we can pass the calling location into the function when using + /// as a command. + #[inline] + pub(crate) fn insert_resource_with_caller( + &mut self, + value: R, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) { let component_id = self.components.init_resource::(); OwningPtr::make(value, |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { - self.insert_resource_by_id(component_id, ptr); + self.insert_resource_by_id( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + caller, + ); } }); } @@ -1320,7 +1382,10 @@ impl World { /// /// Panics if called from a thread other than the main thread. #[inline] + #[track_caller] pub fn init_non_send_resource(&mut self) -> ComponentId { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); let component_id = self.components.init_non_send::(); if self .storages @@ -1332,7 +1397,12 @@ impl World { OwningPtr::make(value, |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { - self.insert_non_send_by_id(component_id, ptr); + self.insert_non_send_by_id( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + caller, + ); } }); } @@ -1349,12 +1419,20 @@ impl World { /// If a value is already present, this function will panic if called /// from a different thread than where the original value was inserted from. #[inline] + #[track_caller] pub fn insert_non_send_resource(&mut self, value: R) { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); let component_id = self.components.init_non_send::(); OwningPtr::make(value, |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { - self.insert_non_send_by_id(component_id, ptr); + self.insert_non_send_by_id( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + caller, + ); } }); } @@ -1363,7 +1441,7 @@ impl World { #[inline] pub fn remove_resource(&mut self) -> Option { let component_id = self.components.get_resource_id(TypeId::of::())?; - let (ptr, _) = self.storages.resources.get_mut(component_id)?.remove()?; + let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?; // SAFETY: `component_id` was gotten via looking up the `R` type unsafe { Some(ptr.read::()) } } @@ -1382,7 +1460,7 @@ impl World { #[inline] pub fn remove_non_send_resource(&mut self) -> Option { let component_id = self.components.get_resource_id(TypeId::of::())?; - let (ptr, _) = self + let (ptr, _, _) = self .storages .non_send_resources .get_mut(component_id)? @@ -1599,10 +1677,13 @@ impl World { /// Gets a mutable reference to the resource of type `T` if it exists, /// otherwise inserts the resource using the result of calling `func`. #[inline] + #[track_caller] pub fn get_resource_or_insert_with( &mut self, func: impl FnOnce() -> R, ) -> Mut<'_, R> { + #[cfg(feature = "track_change_detection")] + let caller = Location::caller(); let change_tick = self.change_tick(); let last_change_tick = self.last_change_tick(); @@ -1612,7 +1693,12 @@ impl World { OwningPtr::make(func(), |ptr| { // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { - data.insert(ptr, change_tick); + data.insert( + ptr, + change_tick, + #[cfg(feature = "track_change_detection")] + caller, + ); } }); } @@ -1696,26 +1782,6 @@ impl World { unsafe { self.as_unsafe_world_cell().get_non_send_resource_mut() } } - // Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. - #[inline] - pub(crate) fn get_resource_archetype_component_id( - &self, - component_id: ComponentId, - ) -> Option { - let resource = self.storages.resources.get(component_id)?; - Some(resource.id()) - } - - // Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. - #[inline] - pub(crate) fn get_non_send_archetype_component_id( - &self, - component_id: ComponentId, - ) -> Option { - let resource = self.storages.non_send_resources.get(component_id)?; - Some(resource.id()) - } - /// For a given batch of ([`Entity`], [`Bundle`]) pairs, either spawns each [`Entity`] with the given /// bundle (if the entity does not exist), or inserts the [`Bundle`] (if the entity already exists). /// This is faster than doing equivalent operations one-by-one. @@ -1745,7 +1811,28 @@ impl World { /// /// assert_eq!(world.get::(e0), Some(&B(0.0))); /// ``` + #[track_caller] pub fn insert_or_spawn_batch(&mut self, iter: I) -> Result<(), Vec> + where + I: IntoIterator, + I::IntoIter: Iterator, + B: Bundle, + { + self.insert_or_spawn_batch_with_caller( + iter, + #[cfg(feature = "track_change_detection")] + Location::caller(), + ) + } + + /// Split into a new function so we can pass the calling location into the function when using + /// as a command. + #[inline] + pub(crate) fn insert_or_spawn_batch_with_caller( + &mut self, + iter: I, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) -> Result<(), Vec> where I: IntoIterator, I::IntoIter: Iterator, @@ -1788,7 +1875,16 @@ impl World { if location.archetype_id == archetype => { // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { inserter.insert(entity, location, bundle) }; + unsafe { + inserter.insert( + entity, + location, + bundle, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; } _ => { // SAFETY: we initialized this bundle_id in `init_info` @@ -1801,7 +1897,16 @@ impl World { ) }; // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { inserter.insert(entity, location, bundle) }; + unsafe { + inserter.insert( + entity, + location, + bundle, + InsertMode::Replace, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; spawn_or_insert = SpawnOrInsert::Insert(inserter, location.archetype_id); } @@ -1810,13 +1915,27 @@ impl World { AllocAtWithoutReplacement::DidNotExist => { if let SpawnOrInsert::Spawn(ref mut spawner) = spawn_or_insert { // SAFETY: `entity` is allocated (but non existent), bundle matches inserter - unsafe { spawner.spawn_non_existent(entity, bundle) }; + unsafe { + spawner.spawn_non_existent( + entity, + bundle, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; } else { // SAFETY: we initialized this bundle_id in `init_info` let mut spawner = unsafe { BundleSpawner::new_with_id(self, bundle_id, change_tick) }; // SAFETY: `entity` is valid, `location` matches entity, bundle matches inserter - unsafe { spawner.spawn_non_existent(entity, bundle) }; + unsafe { + spawner.spawn_non_existent( + entity, + bundle, + #[cfg(feature = "track_change_detection")] + caller, + ) + }; spawn_or_insert = SpawnOrInsert::Spawn(spawner); } } @@ -1856,6 +1975,7 @@ impl World { /// }); /// assert_eq!(world.get_resource::().unwrap().0, 2); /// ``` + #[track_caller] pub fn resource_scope(&mut self, f: impl FnOnce(&mut World, Mut) -> U) -> U { let last_change_tick = self.last_change_tick(); let change_tick = self.change_tick(); @@ -1864,7 +1984,7 @@ impl World { .components .get_resource_id(TypeId::of::()) .unwrap_or_else(|| panic!("resource does not exist: {}", std::any::type_name::())); - let (ptr, mut ticks) = self + let (ptr, mut ticks, mut _caller) = self .storages .resources .get_mut(component_id) @@ -1881,6 +2001,8 @@ impl World { last_run: last_change_tick, this_run: change_tick, }, + #[cfg(feature = "track_change_detection")] + changed_by: &mut _caller, }; let result = f(self, value_mut); assert!(!self.contains_resource::(), @@ -1894,7 +2016,14 @@ impl World { self.storages .resources .get_mut(component_id) - .map(|info| info.insert_with_ticks(ptr, ticks)) + .map(|info| { + info.insert_with_ticks( + ptr, + ticks, + #[cfg(feature = "track_change_detection")] + _caller, + ); + }) .unwrap_or_else(|| { panic!( "No resource of type {} exists in the World.", @@ -1949,17 +2078,24 @@ impl World { /// # Safety /// The value referenced by `value` must be valid for the given [`ComponentId`] of this world. #[inline] + #[track_caller] pub unsafe fn insert_resource_by_id( &mut self, component_id: ComponentId, value: OwningPtr<'_>, + #[cfg(feature = "track_change_detection")] caller: &'static Location, ) { let change_tick = self.change_tick(); let resource = self.initialize_resource_internal(component_id); // SAFETY: `value` is valid for `component_id`, ensured by caller unsafe { - resource.insert(value, change_tick); + resource.insert( + value, + change_tick, + #[cfg(feature = "track_change_detection")] + caller, + ); } } @@ -1976,17 +2112,24 @@ impl World { /// # Safety /// The value referenced by `value` must be valid for the given [`ComponentId`] of this world. #[inline] + #[track_caller] pub unsafe fn insert_non_send_by_id( &mut self, component_id: ComponentId, value: OwningPtr<'_>, + #[cfg(feature = "track_change_detection")] caller: &'static Location, ) { let change_tick = self.change_tick(); let resource = self.initialize_non_send_internal(component_id); // SAFETY: `value` is valid for `component_id`, ensured by caller unsafe { - resource.insert(value, change_tick); + resource.insert( + value, + change_tick, + #[cfg(feature = "track_change_detection")] + caller, + ); } } @@ -2059,9 +2202,16 @@ impl World { } /// Increments the world's current change tick and returns the old value. + /// + /// If you need to call this method, but do not have `&mut` access to the world, + /// consider using [`as_unsafe_world_cell_readonly`](Self::as_unsafe_world_cell_readonly) + /// to obtain an [`UnsafeWorldCell`] and calling [`increment_change_tick`](UnsafeWorldCell::increment_change_tick) on that. + /// Note that this *can* be done in safe code, despite the name of the type. #[inline] - pub fn increment_change_tick(&self) -> Tick { - let prev_tick = self.change_tick.fetch_add(1, Ordering::AcqRel); + pub fn increment_change_tick(&mut self) -> Tick { + let change_tick = self.change_tick.get_mut(); + let prev_tick = *change_tick; + *change_tick = change_tick.wrapping_add(1); Tick::new(prev_tick) } @@ -2198,7 +2348,7 @@ impl World { // By setting the change tick in the drop impl, we ensure that // the change tick gets reset even if a panic occurs during the scope. - impl std::ops::Drop for LastTickGuard<'_> { + impl Drop for LastTickGuard<'_> { fn drop(&mut self) { self.world.last_change_tick = self.last_tick; } @@ -2502,7 +2652,7 @@ impl World { .get_info(component_id) .debug_checked_unwrap() }; - let (ptr, ticks) = data.get_with_ticks()?; + let (ptr, ticks, _caller) = data.get_with_ticks()?; // SAFETY: // - We have exclusive access to the world, so no other code can be aliasing the `TickCells` @@ -2521,6 +2671,11 @@ impl World { // - We iterate one resource at a time, and we let go of each `PtrMut` before getting the next one value: unsafe { ptr.assert_unique() }, ticks, + #[cfg(feature = "track_change_detection")] + // SAFETY: + // - We have exclusive access to the world, so no other code can be aliasing the `Ptr` + // - We iterate one resource at a time, and we let go of each `PtrMut` before getting the next one + changed_by: unsafe { _caller.deref_mut() }, }; Some((component_info, mut_untyped)) @@ -3042,7 +3197,7 @@ mod tests { }; assert!(iter.next().is_none()); - std::mem::drop(iter); + drop(iter); assert_eq!(world.resource::().0, 43); assert_eq!( @@ -3077,7 +3232,12 @@ mod tests { OwningPtr::make(value, |ptr| { // SAFETY: value is valid for the component layout unsafe { - world.insert_resource_by_id(component_id, ptr); + world.insert_resource_by_id( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + core::panic::Location::caller(), + ); } }); diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs new file mode 100644 index 00000000000000..2609d4e4167225 --- /dev/null +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -0,0 +1,337 @@ +//! Provides additional functionality for [`World`] when the `bevy_reflect` feature is enabled. + +use core::any::TypeId; + +use thiserror::Error; + +use bevy_reflect::{Reflect, ReflectFromPtr}; + +use crate::prelude::*; +use crate::world::ComponentId; + +impl World { + /// Retrieves a reference to the given `entity`'s [`Component`] of the given `type_id` using + /// reflection. + /// + /// Requires implementing [`Reflect`] for the [`Component`] (e.g., using [`#[derive(Reflect)`](derive@bevy_reflect::Reflect)) + /// and `app.register_type::()` to have been called[^note-reflect-impl]. + /// + /// If you want to call this with a [`ComponentId`], see [`World::components`] and [`Components::get_id`] to get + /// the corresponding [`TypeId`]. + /// + /// Also see the crate documentation for [`bevy_reflect`] for more information on + /// [`Reflect`] and bevy's reflection capabilities. + /// + /// # Errors + /// + /// See [`GetComponentReflectError`] for the possible errors and their descriptions. + /// + /// # Example + /// + /// ``` + /// use bevy_ecs::prelude::*; + /// use bevy_reflect::Reflect; + /// use std::any::TypeId; + /// + /// // define a `Component` and derive `Reflect` for it + /// #[derive(Component, Reflect)] + /// struct MyComponent; + /// + /// // create a `World` for this example + /// let mut world = World::new(); + /// + /// // Note: This is usually handled by `App::register_type()`, but this example cannot use `App`. + /// world.init_resource::(); + /// world.get_resource_mut::().unwrap().write().register::(); + /// + /// // spawn an entity with a `MyComponent` + /// let entity = world.spawn(MyComponent).id(); + /// + /// // retrieve a reflected reference to the entity's `MyComponent` + /// let comp_reflected: &dyn Reflect = world.get_reflect(entity, TypeId::of::()).unwrap(); + /// + /// // make sure we got the expected type + /// assert!(comp_reflected.is::()); + /// ``` + /// + /// # Note + /// Requires the `bevy_reflect` feature (included in the default features). + /// + /// [`Components::get_id`]: crate::component::Components::get_id + /// [`ReflectFromPtr`]: bevy_reflect::ReflectFromPtr + /// [`TypeData`]: bevy_reflect::TypeData + /// [`Reflect`]: bevy_reflect::Reflect + /// [`App::register_type`]: ../../bevy_app/struct.App.html#method.register_type + /// [^note-reflect-impl]: More specifically: Requires [`TypeData`] for [`ReflectFromPtr`] to be registered for the given `type_id`, + /// which is automatically handled when deriving [`Reflect`] and calling [`App::register_type`]. + #[inline] + pub fn get_reflect( + &self, + entity: Entity, + type_id: TypeId, + ) -> Result<&dyn Reflect, GetComponentReflectError> { + let Some(component_id) = self.components().get_id(type_id) else { + return Err(GetComponentReflectError::NoCorrespondingComponentId( + type_id, + )); + }; + + let Some(comp_ptr) = self.get_by_id(entity, component_id) else { + let component_name = self + .components() + .get_name(component_id) + .map(ToString::to_string); + + return Err(GetComponentReflectError::EntityDoesNotHaveComponent { + entity, + type_id, + component_id, + component_name, + }); + }; + + let Some(type_registry) = self.get_resource::().map(|atr| atr.read()) + else { + return Err(GetComponentReflectError::MissingAppTypeRegistry); + }; + + let Some(reflect_from_ptr) = type_registry.get_type_data::(type_id) else { + return Err(GetComponentReflectError::MissingReflectFromPtrTypeData( + type_id, + )); + }; + + // SAFETY: + // - `comp_ptr` is guaranteed to point to an object of type `type_id` + // - `reflect_from_ptr` was constructed for type `type_id` + // - Assertion that checks this equality is present + unsafe { + assert_eq!( + reflect_from_ptr.type_id(), + type_id, + "Mismatch between Ptr's type_id and ReflectFromPtr's type_id", + ); + + Ok(reflect_from_ptr.as_reflect(comp_ptr)) + } + } + + /// Retrieves a mutable reference to the given `entity`'s [`Component`] of the given `type_id` using + /// reflection. + /// + /// Requires implementing [`Reflect`] for the [`Component`] (e.g., using [`#[derive(Reflect)`](derive@bevy_reflect::Reflect)) + /// and `app.register_type::()` to have been called. + /// + /// This is the mutable version of [`World::get_reflect`], see its docs for more information + /// and an example. + /// + /// Just calling this method does not trigger [change detection](crate::change_detection). + /// + /// # Errors + /// + /// See [`GetComponentReflectError`] for the possible errors and their descriptions. + /// + /// # Example + /// + /// See the documentation for [`World::get_reflect`]. + /// + /// # Note + /// Requires the feature `bevy_reflect` (included in the default features). + /// + /// [`Reflect`]: bevy_reflect::Reflect + #[inline] + pub fn get_reflect_mut( + &mut self, + entity: Entity, + type_id: TypeId, + ) -> Result, GetComponentReflectError> { + // little clone() + read() dance so we a) don't keep a borrow of `self` and b) don't drop a + // temporary (from read()) too early. + let Some(app_type_registry) = self.get_resource::().cloned() else { + return Err(GetComponentReflectError::MissingAppTypeRegistry); + }; + let type_registry = app_type_registry.read(); + + let Some(reflect_from_ptr) = type_registry.get_type_data::(type_id) else { + return Err(GetComponentReflectError::MissingReflectFromPtrTypeData( + type_id, + )); + }; + + let Some(component_id) = self.components().get_id(type_id) else { + return Err(GetComponentReflectError::NoCorrespondingComponentId( + type_id, + )); + }; + + // HACK: Only required for the `None`-case/`else`-branch, but it borrows `self`, which will + // already be mutablyy borrowed by `self.get_mut_by_id()`, and I didn't find a way around it. + let component_name = self + .components() + .get_name(component_id) + .map(ToString::to_string); + + let Some(comp_mut_untyped) = self.get_mut_by_id(entity, component_id) else { + return Err(GetComponentReflectError::EntityDoesNotHaveComponent { + entity, + type_id, + component_id, + component_name, + }); + }; + + // SAFETY: + // - `comp_mut_untyped` is guaranteed to point to an object of type `type_id` + // - `reflect_from_ptr` was constructed for type `type_id` + // - Assertion that checks this equality is present + let comp_mut_typed = comp_mut_untyped.map_unchanged(|ptr_mut| unsafe { + assert_eq!( + reflect_from_ptr.type_id(), + type_id, + "Mismatch between PtrMut's type_id and ReflectFromPtr's type_id", + ); + + reflect_from_ptr.as_reflect_mut(ptr_mut) + }); + + Ok(comp_mut_typed) + } +} + +/// The error type returned by [`World::get_reflect`] and [`World::get_reflect_mut`]. +#[derive(Error, Debug)] +pub enum GetComponentReflectError { + /// There is no [`ComponentId`] corresponding to the given [`TypeId`]. + /// + /// This is usually handled by calling [`App::register_type`] for the type corresponding to + /// the given [`TypeId`]. + /// + /// See the documentation for [`bevy_reflect`] for more information. + /// + /// [`App::register_type`]: ../../../bevy_app/struct.App.html#method.register_type + #[error("No `ComponentId` corresponding to {0:?} found (did you call App::register_type()?)")] + NoCorrespondingComponentId(TypeId), + + /// The given [`Entity`] does not have a [`Component`] corresponding to the given [`TypeId`]. + #[error("The given `Entity` {entity:?} does not have a `{component_name:?}` component ({component_id:?}, which corresponds to {type_id:?})")] + EntityDoesNotHaveComponent { + /// The given [`Entity`]. + entity: Entity, + /// The given [`TypeId`]. + type_id: TypeId, + /// The [`ComponentId`] corresponding to the given [`TypeId`]. + component_id: ComponentId, + /// The name corresponding to the [`Component`] with the given [`TypeId`], or `None` + /// if not available. + component_name: Option, + }, + + /// The [`World`] was missing the [`AppTypeRegistry`] resource. + #[error("The `World` was missing the `AppTypeRegistry` resource")] + MissingAppTypeRegistry, + + /// The [`World`]'s [`TypeRegistry`] did not contain [`TypeData`] for [`ReflectFromPtr`] for the given [`TypeId`]. + /// + /// This is usually handled by calling [`App::register_type`] for the type corresponding to + /// the given [`TypeId`]. + /// + /// See the documentation for [`bevy_reflect`] for more information. + /// + /// [`TypeData`]: bevy_reflect::TypeData + /// [`TypeRegistry`]: bevy_reflect::TypeRegistry + /// [`ReflectFromPtr`]: bevy_reflect::ReflectFromPtr + /// [`App::register_type`]: ../../../bevy_app/struct.App.html#method.register_type + #[error("The `World`'s `TypeRegistry` did not contain `TypeData` for `ReflectFromPtr` for the given {0:?} (did you call `App::register_type()`?)")] + MissingReflectFromPtrTypeData(TypeId), +} + +#[cfg(test)] +mod tests { + use std::any::TypeId; + + use bevy_reflect::Reflect; + + use crate::{ + // For bevy_ecs_macros + self as bevy_ecs, + prelude::{AppTypeRegistry, Component, DetectChanges, World}, + }; + + #[derive(Component, Reflect)] + struct RFoo(i32); + + #[derive(Component)] + struct Bar; + + #[test] + fn get_component_as_reflect() { + let mut world = World::new(); + world.init_resource::(); + + let app_type_registry = world.get_resource_mut::().unwrap(); + app_type_registry.write().register::(); + + { + let entity_with_rfoo = world.spawn(RFoo(42)).id(); + let comp_reflect = world + .get_reflect(entity_with_rfoo, TypeId::of::()) + .expect("Reflection of RFoo-component failed"); + + assert!(comp_reflect.is::()); + } + + { + let entity_without_rfoo = world.spawn_empty().id(); + let reflect_opt = world.get_reflect(entity_without_rfoo, TypeId::of::()); + + assert!(reflect_opt.is_err()); + } + + { + let entity_with_bar = world.spawn(Bar).id(); + let reflect_opt = world.get_reflect(entity_with_bar, TypeId::of::()); + + assert!(reflect_opt.is_err()); + } + } + + #[test] + fn get_component_as_mut_reflect() { + let mut world = World::new(); + world.init_resource::(); + + let app_type_registry = world.get_resource_mut::().unwrap(); + app_type_registry.write().register::(); + + { + let entity_with_rfoo = world.spawn(RFoo(42)).id(); + let mut comp_reflect = world + .get_reflect_mut(entity_with_rfoo, TypeId::of::()) + .expect("Mutable reflection of RFoo-component failed"); + + let comp_rfoo_reflected = comp_reflect + .downcast_mut::() + .expect("Wrong type reflected (expected RFoo)"); + assert_eq!(comp_rfoo_reflected.0, 42); + comp_rfoo_reflected.0 = 1337; + + let rfoo_ref = world.entity(entity_with_rfoo).get_ref::().unwrap(); + assert!(rfoo_ref.is_changed()); + assert_eq!(rfoo_ref.0, 1337); + } + + { + let entity_without_rfoo = world.spawn_empty().id(); + let reflect_opt = world.get_reflect_mut(entity_without_rfoo, TypeId::of::()); + + assert!(reflect_opt.is_err()); + } + + { + let entity_with_bar = world.spawn(Bar).id(); + let reflect_opt = world.get_reflect_mut(entity_with_bar, TypeId::of::()); + + assert!(reflect_opt.is_err()); + } + } +} diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index f6cc9c9a2e6eb7..ea6955a96d9ef9 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -4,6 +4,8 @@ use crate::{ world::World, }; use std::iter::FusedIterator; +#[cfg(feature = "track_change_detection")] +use std::panic::Location; /// An iterator that spawns a series of entities and returns the [ID](Entity) of /// each spawned entity. @@ -16,6 +18,8 @@ where { inner: I, spawner: BundleSpawner<'w>, + #[cfg(feature = "track_change_detection")] + caller: &'static Location<'static>, } impl<'w, I> SpawnBatchIter<'w, I> @@ -24,7 +28,12 @@ where I::Item: Bundle, { #[inline] - pub(crate) fn new(world: &'w mut World, iter: I) -> Self { + #[track_caller] + pub(crate) fn new( + world: &'w mut World, + iter: I, + #[cfg(feature = "track_change_detection")] caller: &'static Location, + ) -> Self { // Ensure all entity allocations are accounted for so `self.entities` can realloc if // necessary world.flush(); @@ -41,6 +50,8 @@ where Self { inner: iter, spawner, + #[cfg(feature = "track_change_detection")] + caller, } } } @@ -69,7 +80,13 @@ where fn next(&mut self) -> Option { let bundle = self.inner.next()?; // SAFETY: bundle matches spawner type - unsafe { Some(self.spawner.spawn(bundle)) } + unsafe { + Some(self.spawner.spawn( + bundle, + #[cfg(feature = "track_change_detection")] + self.caller, + )) + } } fn size_hint(&self) -> (usize, Option) { diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 8de3d3af3e83a2..00b5349d80c1aa 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -6,7 +6,7 @@ use super::{Mut, Ref, World, WorldId}; use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundles, - change_detection::{MutUntyped, Ticks, TicksMut}, + change_detection::{MaybeUnsafeCellLocation, MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, StorageType, Tick, TickCells}, entity::{Entities, Entity, EntityLocation}, observer::Observers, @@ -17,6 +17,8 @@ use crate::{ world::RawCommandQueue, }; use bevy_ptr::Ptr; +#[cfg(feature = "track_change_detection")] +use bevy_ptr::UnsafeCellDeref; use std::{any::TypeId, cell::UnsafeCell, fmt::Debug, marker::PhantomData, ptr}; /// Variant of the [`World`] where resource and component accesses take `&self`, and the responsibility to avoid @@ -81,6 +83,18 @@ unsafe impl Send for UnsafeWorldCell<'_> {} // SAFETY: `&World` and `&mut World` are both `Sync` unsafe impl Sync for UnsafeWorldCell<'_> {} +impl<'w> From<&'w mut World> for UnsafeWorldCell<'w> { + fn from(value: &'w mut World) -> Self { + value.as_unsafe_world_cell() + } +} + +impl<'w> From<&'w World> for UnsafeWorldCell<'w> { + fn from(value: &'w World) -> Self { + value.as_unsafe_world_cell_readonly() + } +} + impl<'w> UnsafeWorldCell<'w> { /// Creates a [`UnsafeWorldCell`] that can be used to access everything immutably #[inline] @@ -255,6 +269,15 @@ impl<'w> UnsafeWorldCell<'w> { unsafe { self.world_metadata() }.read_change_tick() } + /// Returns the id of the last ECS event that was fired. + /// Used internally to ensure observers don't trigger multiple times for the same event. + #[inline] + pub fn last_trigger_id(&self) -> u32 { + // SAFETY: + // - we only access world metadata + unsafe { self.world_metadata() }.last_trigger_id() + } + /// Returns the [`Tick`] indicating the last time that [`World::clear_trackers`] was called. /// /// If this `UnsafeWorldCell` was created from inside of an exclusive system (a [`System`] that @@ -276,7 +299,10 @@ impl<'w> UnsafeWorldCell<'w> { pub fn increment_change_tick(self) -> Tick { // SAFETY: // - we only access world metadata - unsafe { self.world_metadata() }.increment_change_tick() + let change_tick = unsafe { &self.world_metadata().change_tick }; + // NOTE: We can used a relaxed memory ordering here, since nothing + // other than the atomic value itself is relying on atomic synchronization + Tick::new(change_tick.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) } /// Provides unchecked access to the internal data stores of the [`World`]. @@ -331,7 +357,7 @@ impl<'w> UnsafeWorldCell<'w> { // SAFETY: caller ensures `self` has permission to access the resource // caller also ensure that no mutable reference to the resource exists - let (ptr, ticks) = unsafe { self.get_resource_with_ticks(component_id)? }; + let (ptr, ticks, _caller) = unsafe { self.get_resource_with_ticks(component_id)? }; // SAFETY: `component_id` was obtained from the type ID of `R` let value = unsafe { ptr.deref::() }; @@ -340,7 +366,16 @@ impl<'w> UnsafeWorldCell<'w> { let ticks = unsafe { Ticks::from_tick_cells(ticks, self.last_change_tick(), self.change_tick()) }; - Some(Res { value, ticks }) + // SAFETY: caller ensures that no mutable reference to the resource exists + #[cfg(feature = "track_change_detection")] + let caller = unsafe { _caller.deref() }; + + Some(Res { + value, + ticks, + #[cfg(feature = "track_change_detection")] + changed_by: caller, + }) } /// Gets a pointer to the resource with the id [`ComponentId`] if it exists. @@ -443,7 +478,7 @@ impl<'w> UnsafeWorldCell<'w> { ) -> Option> { // SAFETY: we only access data that the caller has ensured is unaliased and `self` // has permission to access. - let (ptr, ticks) = unsafe { self.storages() } + let (ptr, ticks, _caller) = unsafe { self.storages() } .resources .get(component_id)? .get_with_ticks()?; @@ -461,6 +496,11 @@ impl<'w> UnsafeWorldCell<'w> { // - caller ensures that the resource is unaliased value: unsafe { ptr.assert_unique() }, ticks, + #[cfg(feature = "track_change_detection")] + // SAFETY: + // - caller ensures that `self` has permission to access the resource + // - caller ensures that the resource is unaliased + changed_by: unsafe { _caller.deref_mut() }, }) } @@ -505,7 +545,7 @@ impl<'w> UnsafeWorldCell<'w> { let change_tick = self.change_tick(); // SAFETY: we only access data that the caller has ensured is unaliased and `self` // has permission to access. - let (ptr, ticks) = unsafe { self.storages() } + let (ptr, ticks, _caller) = unsafe { self.storages() } .non_send_resources .get(component_id)? .get_with_ticks()?; @@ -520,6 +560,9 @@ impl<'w> UnsafeWorldCell<'w> { // SAFETY: This function has exclusive access to the world so nothing aliases `ptr`. value: unsafe { ptr.assert_unique() }, ticks, + #[cfg(feature = "track_change_detection")] + // SAFETY: This function has exclusive access to the world + changed_by: unsafe { _caller.deref_mut() }, }) } @@ -533,7 +576,7 @@ impl<'w> UnsafeWorldCell<'w> { pub(crate) unsafe fn get_resource_with_ticks( self, component_id: ComponentId, - ) -> Option<(Ptr<'w>, TickCells<'w>)> { + ) -> Option<(Ptr<'w>, TickCells<'w>, MaybeUnsafeCellLocation<'w>)> { // SAFETY: // - caller ensures there is no `&mut World` // - caller ensures there are no mutable borrows of this resource @@ -557,7 +600,7 @@ impl<'w> UnsafeWorldCell<'w> { pub(crate) unsafe fn get_non_send_with_ticks( self, component_id: ComponentId, - ) -> Option<(Ptr<'w>, TickCells<'w>)> { + ) -> Option<(Ptr<'w>, TickCells<'w>, MaybeUnsafeCellLocation<'w>)> { // SAFETY: // - caller ensures there is no `&mut World` // - caller ensures there are no mutable borrows of this resource @@ -729,10 +772,12 @@ impl<'w> UnsafeEntityCell<'w> { self.entity, self.location, ) - .map(|(value, cells)| Ref { + .map(|(value, cells, _caller)| Ref { // SAFETY: returned component is of type T value: value.deref::(), ticks: Ticks::from_tick_cells(cells, last_change_tick, change_tick), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref(), }) } } @@ -828,10 +873,12 @@ impl<'w> UnsafeEntityCell<'w> { self.entity, self.location, ) - .map(|(value, cells)| Mut { + .map(|(value, cells, _caller)| Mut { // SAFETY: returned component is of type T value: value.assert_unique().deref_mut::(), ticks: TicksMut::from_tick_cells(cells, last_change_tick, change_tick), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref_mut(), }) } } @@ -888,7 +935,7 @@ impl<'w> UnsafeEntityCell<'w> { self.entity, self.location, ) - .map(|(value, cells)| MutUntyped { + .map(|(value, cells, _caller)| MutUntyped { // SAFETY: world access validated by caller and ties world lifetime to `MutUntyped` lifetime value: value.assert_unique(), ticks: TicksMut::from_tick_cells( @@ -896,6 +943,8 @@ impl<'w> UnsafeEntityCell<'w> { self.world.last_change_tick(), self.world.change_tick(), ), + #[cfg(feature = "track_change_detection")] + changed_by: _caller.deref_mut(), }) } } @@ -972,7 +1021,7 @@ unsafe fn get_component_and_ticks( storage_type: StorageType, entity: Entity, location: EntityLocation, -) -> Option<(Ptr<'_>, TickCells<'_>)> { +) -> Option<(Ptr<'_>, TickCells<'_>, MaybeUnsafeCellLocation<'_>)> { match storage_type { StorageType::Table => { let components = world.fetch_table(location, component_id)?; @@ -984,6 +1033,10 @@ unsafe fn get_component_and_ticks( added: components.get_added_tick_unchecked(location.table_row), changed: components.get_changed_tick_unchecked(location.table_row), }, + #[cfg(feature = "track_change_detection")] + components.get_changed_by_unchecked(location.table_row), + #[cfg(not(feature = "track_change_detection"))] + (), )) } StorageType::SparseSet => world.fetch_sparse_set(component_id)?.get_with_ticks(entity), diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index 09ce3e4b3280d2..8a8eb22fa9f1de 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -19,5 +19,5 @@ encase_derive_impl = "0.8" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index b15a00c2631097..21b4b83e99f47d 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -24,5 +24,5 @@ thiserror = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index df5110a0792f5a..5f79b93d3a266d 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["bevy"] [features] webgl = [] webgpu = [] +bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] [dependencies] # Bevy @@ -21,10 +22,10 @@ bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.15.0-dev", optional = true } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev", optional = true } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_gizmos_macros = { path = "macros", version = "0.15.0-dev" } bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } @@ -35,5 +36,5 @@ bytemuck = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index 6ef35688b4edcb..97aebb4d894ba9 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -23,5 +23,5 @@ quote = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_gizmos/src/arcs.rs b/crates/bevy_gizmos/src/arcs.rs index c2b8b1f325cd39..14c260dd307465 100644 --- a/crates/bevy_gizmos/src/arcs.rs +++ b/crates/bevy_gizmos/src/arcs.rs @@ -31,7 +31,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use std::f32::consts::PI; /// # use bevy_color::palettes::basic::{GREEN, RED}; @@ -168,7 +167,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use std::f32::consts::PI; /// # use bevy_color::palettes::css::ORANGE; @@ -225,7 +223,6 @@ where /// # Examples /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::css::ORANGE; /// fn system(mut gizmos: Gizmos) { @@ -272,7 +269,6 @@ where /// # Examples /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::css::ORANGE; /// fn system(mut gizmos: Gizmos) { diff --git a/crates/bevy_gizmos/src/arrows.rs b/crates/bevy_gizmos/src/arrows.rs index 4bde0a7f60b623..29d5119e79377f 100644 --- a/crates/bevy_gizmos/src/arrows.rs +++ b/crates/bevy_gizmos/src/arrows.rs @@ -36,7 +36,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -114,7 +113,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -146,7 +144,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { diff --git a/crates/bevy_gizmos/src/circles.rs b/crates/bevy_gizmos/src/circles.rs index 216105240da3ba..1f1d2bac217605 100644 --- a/crates/bevy_gizmos/src/circles.rs +++ b/crates/bevy_gizmos/src/circles.rs @@ -31,7 +31,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -70,7 +69,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -109,7 +107,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -148,7 +145,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -186,7 +182,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::Color; /// fn system(mut gizmos: Gizmos) { @@ -212,7 +207,7 @@ where gizmos: self, radius, position, - rotation, + rotation: rotation.normalize(), color: color.into(), resolution: DEFAULT_CIRCLE_RESOLUTION, } diff --git a/crates/bevy_gizmos/src/config.rs b/crates/bevy_gizmos/src/config.rs index c3030d9a3488fc..0705029ddf19c0 100644 --- a/crates/bevy_gizmos/src/config.rs +++ b/crates/bevy_gizmos/src/config.rs @@ -3,9 +3,14 @@ use crate as bevy_gizmos; pub use bevy_gizmos_macros::GizmoConfigGroup; -use bevy_ecs::{component::Component, reflect::ReflectResource, system::Resource}; +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] +use bevy_ecs::component::Component; + +use bevy_ecs::{reflect::ReflectResource, system::Resource}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; -use bevy_render::view::RenderLayers; use bevy_utils::TypeIdMap; use core::panic; use std::{ @@ -164,7 +169,8 @@ pub struct GizmoConfig { /// Describes which rendering layers gizmos will be rendered to. /// /// Gizmos will only be rendered to cameras with intersecting layers. - pub render_layers: RenderLayers, + #[cfg(feature = "bevy_render")] + pub render_layers: bevy_render::view::RenderLayers, /// Describe how lines should join pub line_joints: GizmoLineJoint, @@ -178,6 +184,7 @@ impl Default for GizmoConfig { line_perspective: false, line_style: GizmoLineStyle::Solid, depth_bias: 0., + #[cfg(feature = "bevy_render")] render_layers: Default::default(), line_joints: GizmoLineJoint::None, @@ -185,13 +192,21 @@ impl Default for GizmoConfig { } } +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] #[derive(Component)] pub(crate) struct GizmoMeshConfig { pub line_perspective: bool, pub line_style: GizmoLineStyle, - pub render_layers: RenderLayers, + pub render_layers: bevy_render::view::RenderLayers, } +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] impl From<&GizmoConfig> for GizmoMeshConfig { fn from(item: &GizmoConfig) -> Self { GizmoMeshConfig { diff --git a/crates/bevy_gizmos/src/cross.rs b/crates/bevy_gizmos/src/cross.rs index 4d716a9dd4a289..282f6c3dfde5e6 100644 --- a/crates/bevy_gizmos/src/cross.rs +++ b/crates/bevy_gizmos/src/cross.rs @@ -18,7 +18,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::WHITE; /// fn system(mut gizmos: Gizmos) { @@ -51,7 +50,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::WHITE; /// fn system(mut gizmos: Gizmos) { diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index bcd3ef0d5a5a01..bd32ba8f1fb040 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -280,7 +280,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -304,7 +303,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -334,7 +332,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -357,7 +354,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -386,7 +382,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -417,7 +412,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{BLUE, GREEN, RED}; /// fn system(mut gizmos: Gizmos) { @@ -465,7 +459,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -489,7 +482,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_transform::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -530,7 +522,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -553,7 +544,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -582,7 +572,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -609,7 +598,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN, BLUE}; /// fn system(mut gizmos: Gizmos) { @@ -643,7 +631,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -666,7 +653,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::{RED, GREEN}; /// fn system(mut gizmos: Gizmos) { @@ -695,7 +681,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index 6aea9914823e8a..b65137205befb5 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -4,8 +4,8 @@ //! and assorted support items. use crate::prelude::{GizmoConfigGroup, Gizmos}; -use bevy_color::LinearRgba; -use bevy_math::{Quat, UVec2, UVec3, Vec2, Vec3}; +use bevy_color::Color; +use bevy_math::{Quat, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles}; /// A builder returned by [`Gizmos::grid_3d`] pub struct GridBuilder3d<'a, 'w, 's, Config, Clear> @@ -20,7 +20,7 @@ where cell_count: UVec3, skew: Vec3, outer_edges: [bool; 3], - color: LinearRgba, + color: Color, } /// A builder returned by [`Gizmos::grid`] and [`Gizmos::grid_2d`] pub struct GridBuilder2d<'a, 'w, 's, Config, Clear> @@ -35,7 +35,7 @@ where cell_count: UVec2, skew: Vec2, outer_edges: [bool; 2], - color: LinearRgba, + color: Color, } impl GridBuilder3d<'_, '_, '_, Config, Clear> @@ -201,7 +201,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -223,7 +222,7 @@ where rotation: Quat, cell_count: UVec2, spacing: Vec2, - color: impl Into, + color: impl Into, ) -> GridBuilder2d<'_, 'w, 's, Config, Clear> { GridBuilder2d { gizmos: self, @@ -257,7 +256,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -279,7 +277,7 @@ where rotation: Quat, cell_count: UVec3, spacing: Vec3, - color: impl Into, + color: impl Into, ) -> GridBuilder3d<'_, 'w, 's, Config, Clear> { GridBuilder3d { gizmos: self, @@ -313,7 +311,6 @@ where /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::basic::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -335,7 +332,7 @@ where rotation: f32, cell_count: UVec2, spacing: Vec2, - color: impl Into, + color: impl Into, ) -> GridBuilder2d<'_, 'w, 's, Config, Clear> { GridBuilder2d { gizmos: self, @@ -359,7 +356,7 @@ fn draw_grid( cell_count: UVec3, skew: Vec3, outer_edges: [bool; 3], - color: LinearRgba, + color: Color, ) where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, @@ -368,78 +365,71 @@ fn draw_grid( return; } + #[inline] + fn or_zero(cond: bool, val: Vec3) -> Vec3 { + if cond { + val + } else { + Vec3::ZERO + } + } + // Offset between two adjacent grid cells along the x/y-axis and accounting for skew. - let dx = spacing.x - * Vec3::new(1., skew.y.tan(), skew.z.tan()) - * if cell_count.x != 0 { 1. } else { 0. }; - let dy = spacing.y - * Vec3::new(skew.x.tan(), 1., skew.z.tan()) - * if cell_count.y != 0 { 1. } else { 0. }; - let dz = spacing.z - * Vec3::new(skew.x.tan(), skew.y.tan(), 1.) - * if cell_count.z != 0 { 1. } else { 0. }; + let skew_tan = Vec3::from(skew.to_array().map(f32::tan)); + let dx = or_zero( + cell_count.x != 0, + spacing.x * Vec3::new(1., skew_tan.y, skew_tan.z), + ); + let dy = or_zero( + cell_count.y != 0, + spacing.y * Vec3::new(skew_tan.x, 1., skew_tan.z), + ); + let dz = or_zero( + cell_count.z != 0, + spacing.z * Vec3::new(skew_tan.x, skew_tan.y, 1.), + ); // Bottom-left-front corner of the grid - let grid_start = position - - cell_count.x as f32 / 2.0 * dx - - cell_count.y as f32 / 2.0 * dy - - cell_count.z as f32 / 2.0 * dz; + let cell_count_half = cell_count.as_vec3() * 0.5; + let grid_start = -cell_count_half.x * dx - cell_count_half.y * dy - cell_count_half.z * dz; - let line_count = UVec3::new( - if outer_edges[0] { - cell_count.x + 1 - } else { - cell_count.x.saturating_sub(1) - }, - if outer_edges[1] { - cell_count.y + 1 - } else { - cell_count.y.saturating_sub(1) - }, - if outer_edges[2] { - cell_count.z + 1 - } else { - cell_count.z.saturating_sub(1) - }, - ); - let x_start = grid_start + if outer_edges[0] { Vec3::ZERO } else { dy + dz }; - let y_start = grid_start + if outer_edges[1] { Vec3::ZERO } else { dx + dz }; - let z_start = grid_start + if outer_edges[2] { Vec3::ZERO } else { dx + dy }; + let outer_edges_u32 = UVec3::from(outer_edges.map(|v| v as u32)); + let line_count = outer_edges_u32 * cell_count.saturating_add(UVec3::ONE) + + (UVec3::ONE - outer_edges_u32) * cell_count.saturating_sub(UVec3::ONE); - // Lines along the x direction - let dline = dx * cell_count.x as f32; - for iy in 0..line_count.y { - let iy = iy as f32; - for iz in 0..line_count.z { - let iz = iz as f32; - let line_start = x_start + iy * dy + iz * dz; - let line_end = line_start + dline; + let x_start = grid_start + or_zero(!outer_edges[0], dy + dz); + let y_start = grid_start + or_zero(!outer_edges[1], dx + dz); + let z_start = grid_start + or_zero(!outer_edges[2], dx + dy); - gizmos.line(rotation * line_start, rotation * line_end, color); - } + fn iter_lines( + delta_a: Vec3, + delta_b: Vec3, + delta_c: Vec3, + line_count: UVec2, + cell_count: u32, + start: Vec3, + ) -> impl Iterator { + let dline = delta_a * cell_count as f32; + (0..line_count.x).map(|v| v as f32).flat_map(move |b| { + (0..line_count.y).map(|v| v as f32).map(move |c| { + let line_start = start + b * delta_b + c * delta_c; + let line_end = line_start + dline; + [line_start, line_end] + }) + }) } - // Lines along the y direction - let dline = dy * cell_count.y as f32; - for ix in 0..line_count.x { - let ix = ix as f32; - for iz in 0..line_count.z { - let iz = iz as f32; - let line_start = y_start + ix * dx + iz * dz; - let line_end = line_start + dline; - gizmos.line(rotation * line_start, rotation * line_end, color); - } - } + // Lines along the x direction + let x_lines = iter_lines(dx, dy, dz, line_count.yz(), cell_count.x, x_start); + // Lines along the y direction + let y_lines = iter_lines(dy, dz, dx, line_count.zx(), cell_count.y, y_start); // Lines along the z direction - let dline = dz * cell_count.z as f32; - for ix in 0..line_count.x { - let ix = ix as f32; - for iy in 0..line_count.y { - let iy = iy as f32; - let line_start = z_start + ix * dx + iy * dy; - let line_end = line_start + dline; - - gizmos.line(rotation * line_start, rotation * line_end, color); - } - } + let z_lines = iter_lines(dz, dx, dy, line_count.xy(), cell_count.z, z_start); + x_lines + .chain(y_lines) + .chain(z_lines) + .map(|ps| ps.map(|p| position + rotation * p)) + .for_each(|[start, end]| { + gizmos.line(start, end, color); + }); } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 1084cce1a6052f..1d42c589470ba3 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -9,7 +9,6 @@ //! # Example //! ``` //! # use bevy_gizmos::prelude::*; -//! # use bevy_render::prelude::*; //! # use bevy_math::prelude::*; //! # use bevy_color::palettes::basic::GREEN; //! fn system(mut gizmos: Gizmos) { @@ -31,6 +30,7 @@ pub enum GizmoRenderSystem { QueueLineGizmos3d, } +#[cfg(feature = "bevy_render")] pub mod aabb; pub mod arcs; pub mod arrows; @@ -42,19 +42,20 @@ pub mod grid; pub mod primitives; pub mod rounded_box; -#[cfg(feature = "bevy_pbr")] +#[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] pub mod light; -#[cfg(feature = "bevy_sprite")] +#[cfg(all(feature = "bevy_sprite", feature = "bevy_render"))] mod pipeline_2d; -#[cfg(feature = "bevy_pbr")] +#[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] mod pipeline_3d; /// The `bevy_gizmos` prelude. pub mod prelude { + #[cfg(feature = "bevy_render")] + pub use crate::aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}; #[doc(hidden)] pub use crate::{ - aabb::{AabbGizmoConfigGroup, ShowAabbGizmo}, config::{ DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoLineJoint, GizmoLineStyle, @@ -64,25 +65,36 @@ pub mod prelude { AppGizmoBuilder, }; - #[cfg(feature = "bevy_pbr")] + #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] pub use crate::light::{LightGizmoColor, LightGizmoConfigGroup, ShowLightGizmo}; } -use aabb::AabbGizmoPlugin; -use bevy_app::{App, FixedFirst, FixedLast, Last, Plugin, RunFixedMainLoop}; -use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; -use bevy_color::LinearRgba; +#[cfg(feature = "bevy_render")] use bevy_ecs::{ - component::Component, query::ROQueryItem, - schedule::{IntoSystemConfigs, SystemSet}, system::{ lifetimeless::{Read, SRes}, - Commands, Res, ResMut, Resource, SystemParamItem, + Commands, SystemParamItem, }, }; + +use bevy_app::{App, FixedFirst, FixedLast, Last, Plugin, RunFixedMainLoop}; +use bevy_asset::{Asset, AssetApp, Assets, Handle}; +use bevy_color::LinearRgba; +#[cfg(feature = "bevy_render")] +use bevy_ecs::component::Component; +use bevy_ecs::{ + schedule::{IntoSystemConfigs, SystemSet}, + system::{Res, ResMut, Resource}, +}; use bevy_math::Vec3; use bevy_reflect::TypePath; +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite"), +))] +use bevy_render::render_resource::{VertexAttribute, VertexBufferLayout, VertexStepMode}; +#[cfg(feature = "bevy_render")] use bevy_render::{ extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, @@ -90,86 +102,92 @@ use bevy_render::{ render_resource::{ binding_types::uniform_buffer, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, Buffer, BufferInitDescriptor, BufferUsages, Shader, ShaderStages, - ShaderType, VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode, + ShaderType, VertexFormat, }, renderer::RenderDevice, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; + use bevy_time::Fixed; use bevy_utils::TypeIdMap; +#[cfg(feature = "bevy_render")] use bytemuck::cast_slice; use config::{ DefaultGizmoConfigGroup, GizmoConfig, GizmoConfigGroup, GizmoConfigStore, GizmoLineJoint, - GizmoMeshConfig, }; use gizmos::{GizmoStorage, Swap}; -#[cfg(feature = "bevy_pbr")] +#[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] use light::LightGizmoPlugin; use std::{any::TypeId, mem}; +#[cfg(feature = "bevy_render")] const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(7414812689238026784); +#[cfg(feature = "bevy_render")] const LINE_JOINT_SHADER_HANDLE: Handle = Handle::weak_from_u128(1162780797909187908); /// A [`Plugin`] that provides an immediate mode drawing api for visual debugging. /// /// Requires to be loaded after [`PbrPlugin`](bevy_pbr::PbrPlugin) or [`SpritePlugin`](bevy_sprite::SpritePlugin). +#[derive(Default)] 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_utils::tracing::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); - load_internal_asset!( - app, - LINE_JOINT_SHADER_HANDLE, - "line_joints.wgsl", - Shader::from_wgsl - ); + fn build(&self, app: &mut App) { + #[cfg(feature = "bevy_render")] + { + use bevy_asset::load_internal_asset; + load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); + load_internal_asset!( + app, + LINE_JOINT_SHADER_HANDLE, + "line_joints.wgsl", + Shader::from_wgsl + ); + } app.register_type::() .register_type::() - .add_plugins(UniformComponentPlugin::::default()) .init_asset::() - .add_plugins(RenderAssetPlugin::::default()) .init_resource::() // We insert the Resource GizmoConfigStore into the world implicitly here if it does not exist. - .init_gizmo_group::() - .add_plugins(AabbGizmoPlugin); + .init_gizmo_group::(); - #[cfg(feature = "bevy_pbr")] - app.add_plugins(LightGizmoPlugin); - - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; + #[cfg(feature = "bevy_render")] + app.add_plugins(aabb::AabbGizmoPlugin) + .add_plugins(UniformComponentPlugin::::default()) + .add_plugins(RenderAssetPlugin::::default()); - render_app.add_systems( - Render, - prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), - ); + #[cfg(all(feature = "bevy_pbr", feature = "bevy_render"))] + app.add_plugins(LightGizmoPlugin); - render_app.add_systems(ExtractSchedule, extract_gizmo_data); + #[cfg(feature = "bevy_render")] + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_systems( + Render, + prepare_line_gizmo_bind_group.in_set(RenderSet::PrepareBindGroups), + ); - #[cfg(feature = "bevy_sprite")] - if app.is_plugin_added::() { - app.add_plugins(pipeline_2d::LineGizmo2dPlugin); - } else { - bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); - } - #[cfg(feature = "bevy_pbr")] - if app.is_plugin_added::() { - app.add_plugins(pipeline_3d::LineGizmo3dPlugin); + render_app.add_systems(ExtractSchedule, extract_gizmo_data); + + #[cfg(feature = "bevy_sprite")] + if app.is_plugin_added::() { + app.add_plugins(pipeline_2d::LineGizmo2dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); + } + #[cfg(feature = "bevy_pbr")] + if app.is_plugin_added::() { + app.add_plugins(pipeline_3d::LineGizmo3dPlugin); + } else { + bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); + } } else { - bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); + bevy_utils::tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); } } - fn finish(&self, app: &mut bevy_app::App) { + #[cfg(feature = "bevy_render")] + fn finish(&self, app: &mut App) { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; @@ -228,13 +246,15 @@ impl AppGizmoBuilder for App { .init_resource::>>() .add_systems( RunFixedMainLoop, - start_gizmo_context::.before(bevy_time::run_fixed_main_schedule), + start_gizmo_context:: + .in_set(bevy_app::RunFixedMainLoopSystem::BeforeFixedMainLoop), ) .add_systems(FixedFirst, clear_gizmo_context::) .add_systems(FixedLast, collect_requested_gizmos::) .add_systems( RunFixedMainLoop, - end_gizmo_context::.after(bevy_time::run_fixed_main_schedule), + end_gizmo_context:: + .in_set(bevy_app::RunFixedMainLoopSystem::AfterFixedMainLoop), ) .add_systems( Last, @@ -360,14 +380,14 @@ fn update_gizmo_meshes( list.positions = mem::take(&mut storage.list_positions); list.colors = mem::take(&mut storage.list_colors); } else { - let mut list = LineGizmo { + let list = LineGizmo { strip: false, - ..Default::default() + config_ty: TypeId::of::(), + positions: mem::take(&mut storage.list_positions), + colors: mem::take(&mut storage.list_colors), + joints: GizmoLineJoint::None, }; - list.positions = mem::take(&mut storage.list_positions); - list.colors = mem::take(&mut storage.list_colors); - *handle = Some(line_gizmos.add(list)); } } @@ -383,20 +403,20 @@ fn update_gizmo_meshes( strip.colors = mem::take(&mut storage.strip_colors); strip.joints = config.line_joints; } else { - let mut strip = LineGizmo { + let strip = LineGizmo { strip: true, joints: config.line_joints, - ..Default::default() + config_ty: TypeId::of::(), + positions: mem::take(&mut storage.strip_positions), + colors: mem::take(&mut storage.strip_colors), }; - strip.positions = mem::take(&mut storage.strip_positions); - strip.colors = mem::take(&mut storage.strip_colors); - *handle = Some(line_gizmos.add(strip)); } } } +#[cfg(feature = "bevy_render")] fn extract_gizmo_data( mut commands: Commands, handles: Extract>, @@ -430,11 +450,13 @@ fn extract_gizmo_data( _padding: Default::default(), }, (*handle).clone_weak(), - GizmoMeshConfig::from(config), + #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] + config::GizmoMeshConfig::from(config), )); } } +#[cfg(feature = "bevy_render")] #[derive(Component, ShaderType, Clone, Copy)] struct LineGizmoUniform { line_width: f32, @@ -446,16 +468,22 @@ struct LineGizmoUniform { _padding: f32, } -#[derive(Asset, Debug, Default, Clone, TypePath)] -struct LineGizmo { - positions: Vec, - colors: Vec, +/// A gizmo asset that represents a line. +#[derive(Asset, Debug, Clone, TypePath)] +pub struct LineGizmo { + /// Positions of the gizmo's vertices + pub positions: Vec, + /// Colors of the gizmo's vertices + pub colors: Vec, /// Whether this gizmo's topology is a line-strip or line-list - strip: bool, + pub strip: bool, /// Whether this gizmo should draw line joints. This is only applicable if the gizmo's topology is line-strip. - joints: GizmoLineJoint, + pub joints: GizmoLineJoint, + /// The type of the gizmo's configuration group + pub config_ty: TypeId, } +#[cfg(feature = "bevy_render")] #[derive(Debug, Clone)] struct GpuLineGizmo { position_buffer: Buffer, @@ -465,6 +493,7 @@ struct GpuLineGizmo { joints: GizmoLineJoint, } +#[cfg(feature = "bevy_render")] impl RenderAsset for GpuLineGizmo { type SourceAsset = LineGizmo; type Param = SRes; @@ -497,16 +526,19 @@ impl RenderAsset for GpuLineGizmo { } } +#[cfg(feature = "bevy_render")] #[derive(Resource)] struct LineGizmoUniformBindgroupLayout { layout: BindGroupLayout, } +#[cfg(feature = "bevy_render")] #[derive(Resource)] struct LineGizmoUniformBindgroup { bindgroup: BindGroup, } +#[cfg(feature = "bevy_render")] fn prepare_line_gizmo_bind_group( mut commands: Commands, line_gizmo_uniform_layout: Res, @@ -524,7 +556,9 @@ fn prepare_line_gizmo_bind_group( } } +#[cfg(feature = "bevy_render")] struct SetLineGizmoBindGroup; +#[cfg(feature = "bevy_render")] impl RenderCommand

for SetLineGizmoBindGroup { type Param = SRes; type ViewQuery = (); @@ -539,7 +573,7 @@ impl RenderCommand

for SetLineGizmoBindGroup pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(uniform_index) = uniform_index else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_bind_group( I, @@ -550,7 +584,9 @@ impl RenderCommand

for SetLineGizmoBindGroup } } +#[cfg(feature = "bevy_render")] struct DrawLineGizmo; +#[cfg(feature = "bevy_render")] impl RenderCommand

for DrawLineGizmo { type Param = SRes>; type ViewQuery = (); @@ -565,10 +601,10 @@ impl RenderCommand

for DrawLineGizmo { pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(handle) = handle else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; if line_gizmo.vertex_count < 2 { @@ -576,11 +612,16 @@ impl RenderCommand

for DrawLineGizmo { } let instances = if line_gizmo.strip { - pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..)); - pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..)); + let item_size = VertexFormat::Float32x3.size(); + let buffer_size = line_gizmo.position_buffer.size() - item_size; - pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..)); - pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..)); + pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size)); + pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(item_size..)); + + let item_size = VertexFormat::Float32x4.size(); + let buffer_size = line_gizmo.color_buffer.size() - item_size; + pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..buffer_size)); + pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..)); u32::max(line_gizmo.vertex_count, 1) - 1 } else { @@ -596,7 +637,9 @@ impl RenderCommand

for DrawLineGizmo { } } +#[cfg(feature = "bevy_render")] struct DrawLineJointGizmo; +#[cfg(feature = "bevy_render")] impl RenderCommand

for DrawLineJointGizmo { type Param = SRes>; type ViewQuery = (); @@ -611,10 +654,10 @@ impl RenderCommand

for DrawLineJointGizmo { pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(handle) = handle else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(line_gizmo) = line_gizmos.into_inner().get(handle) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; if line_gizmo.vertex_count <= 2 || !line_gizmo.strip { @@ -648,6 +691,10 @@ impl RenderCommand

for DrawLineJointGizmo { } } +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { use VertexFormat::*; let mut position_layout = VertexBufferLayout { @@ -675,13 +722,11 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { position_layout.clone(), { position_layout.attributes[0].shader_location = 1; - position_layout.attributes[0].offset = Float32x3.size(); position_layout }, color_layout.clone(), { color_layout.attributes[0].shader_location = 3; - color_layout.attributes[0].offset = Float32x4.size(); color_layout }, ] @@ -704,6 +749,10 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec { } } +#[cfg(all( + feature = "bevy_render", + any(feature = "bevy_pbr", feature = "bevy_sprite") +))] fn line_joint_gizmo_vertex_buffer_layouts() -> Vec { use VertexFormat::*; let mut position_layout = VertexBufferLayout { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index df84a48256cd0e..0f6552f7874065 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -7,7 +7,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_asset::Handle; -use bevy_core_pipeline::core_2d::Transparent2d; +use bevy_core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}; use bevy_ecs::{ prelude::Entity, @@ -139,7 +139,22 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { }), layout, primitive: PrimitiveState::default(), - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.mesh_key.msaa_samples(), mask: !0, @@ -224,7 +239,22 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { }), layout, primitive: PrimitiveState::default(), - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.mesh_key.msaa_samples(), mask: !0, @@ -255,15 +285,14 @@ fn queue_line_gizmos_2d( pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - msaa: Res, line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, Option<&RenderLayers>)>, + mut views: Query<(Entity, &ExtractedView, &Msaa, Option<&RenderLayers>)>, ) { let draw_function = draw_functions.read().get_id::().unwrap(); - for (view_entity, view, render_layers) in &mut views { + for (view_entity, view, msaa, render_layers) in &mut views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; @@ -309,18 +338,17 @@ fn queue_line_joint_gizmos_2d( pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - msaa: Res, line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, Option<&RenderLayers>)>, + mut views: Query<(Entity, &ExtractedView, &Msaa, Option<&RenderLayers>)>, ) { let draw_function = draw_functions .read() .get_id::() .unwrap(); - for (view_entity, view, render_layers) in &mut views { + for (view_entity, view, msaa, render_layers) in &mut views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 761278ca3e1cbc..8197623b3618cf 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -280,13 +280,13 @@ fn queue_line_gizmos_3d( pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - msaa: Res, line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, mut views: Query<( Entity, &ExtractedView, + &Msaa, Option<&RenderLayers>, ( Has, @@ -301,6 +301,7 @@ fn queue_line_gizmos_3d( for ( view_entity, view, + msaa, render_layers, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), ) in &mut views @@ -368,13 +369,13 @@ fn queue_line_joint_gizmos_3d( pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - msaa: Res, line_gizmos: Query<(Entity, &Handle, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, mut views: Query<( Entity, &ExtractedView, + &Msaa, Option<&RenderLayers>, ( Has, @@ -392,6 +393,7 @@ fn queue_line_joint_gizmos_3d( for ( view_entity, view, + msaa, render_layers, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), ) in &mut views diff --git a/crates/bevy_gizmos/src/rounded_box.rs b/crates/bevy_gizmos/src/rounded_box.rs index d4dbd54c1a1b4f..232bcee6dfb05b 100644 --- a/crates/bevy_gizmos/src/rounded_box.rs +++ b/crates/bevy_gizmos/src/rounded_box.rs @@ -227,7 +227,6 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::css::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -283,7 +282,6 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::css::GREEN; /// fn system(mut gizmos: Gizmos) { @@ -339,7 +337,6 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Example /// ``` /// # use bevy_gizmos::prelude::*; - /// # use bevy_render::prelude::*; /// # use bevy_math::prelude::*; /// # use bevy_color::palettes::css::GREEN; /// fn system(mut gizmos: Gizmos) { diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 18174261080791..6bc73daa1688ce 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -9,10 +9,12 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -dds = ["bevy_render/dds"] +dds = ["bevy_render/dds", "bevy_core_pipeline/dds"] pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"] -pbr_multi_layer_material_textures = [] -pbr_anisotropy_texture = [] +pbr_multi_layer_material_textures = [ + "bevy_pbr/pbr_multi_layer_material_textures", +] +pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"] [dependencies] # bevy @@ -65,5 +67,5 @@ bevy_log = { path = "../bevy_log", version = "0.15.0-dev" } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 1fa19666926e40..722dde2150d590 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -109,7 +109,7 @@ use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_pbr::StandardMaterial; use bevy_reflect::{Reflect, TypePath}; use bevy_render::{ - mesh::{Mesh, MeshVertexAttribute}, + mesh::{skinning::SkinnedMeshInverseBindposes, Mesh, MeshVertexAttribute}, renderer::RenderDevice, texture::CompressedImageFormats, }; @@ -153,6 +153,7 @@ impl Plugin for GltfPlugin { .init_asset::() .init_asset::() .init_asset::() + .init_asset::() .preregister_asset_loader::(&["gltf", "glb"]); } @@ -187,6 +188,10 @@ pub struct Gltf { pub nodes: Vec>, /// Named nodes loaded from the glTF file. pub named_nodes: HashMap, Handle>, + /// All skins loaded from the glTF file. + pub skins: Vec>, + /// Named skins loaded from the glTF file. + pub named_skins: HashMap, Handle>, /// Default scene to be displayed. pub default_scene: Option>, /// All animations loaded from the glTF file. @@ -200,7 +205,8 @@ pub struct Gltf { } /// A glTF node with all of its child nodes, its [`GltfMesh`], -/// [`Transform`](bevy_transform::prelude::Transform) and an optional [`GltfExtras`]. +/// [`Transform`](bevy_transform::prelude::Transform), its optional [`GltfSkin`] +/// and an optional [`GltfExtras`]. /// /// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-node). #[derive(Asset, Debug, Clone, TypePath)] @@ -213,8 +219,13 @@ pub struct GltfNode { pub children: Vec>, /// Mesh of the node. pub mesh: Option>, + /// Skin of the node. + pub skin: Option>, /// Local transform. pub transform: bevy_transform::prelude::Transform, + /// Is this node used as an animation root + #[cfg(feature = "bevy_animation")] + pub is_animation_root: bool, /// Additional data. pub extras: Option, } @@ -226,6 +237,7 @@ impl GltfNode { children: Vec>, mesh: Option>, transform: bevy_transform::prelude::Transform, + skin: Option>, extras: Option, ) -> Self { Self { @@ -238,16 +250,73 @@ impl GltfNode { children, mesh, transform, + skin, + #[cfg(feature = "bevy_animation")] + is_animation_root: false, extras, } } + /// Create a node with animation root mark + #[cfg(feature = "bevy_animation")] + pub fn with_animation_root(self, is_animation_root: bool) -> Self { + Self { + is_animation_root, + ..self + } + } + /// Subasset label for this node within the gLTF parent asset. pub fn asset_label(&self) -> GltfAssetLabel { GltfAssetLabel::Node(self.index) } } +/// A glTF skin with all of its joint nodes, [`SkinnedMeshInversiveBindposes`](bevy_render::mesh::skinning::SkinnedMeshInverseBindposes) +/// and an optional [`GltfExtras`]. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-skin). +#[derive(Asset, Debug, Clone, TypePath)] +pub struct GltfSkin { + /// Index of the skin inside the scene + pub index: usize, + /// Computed name for a skin - either a user defined skin name from gLTF or a generated name from index + pub name: String, + /// All the nodes that form this skin. + pub joints: Vec>, + /// Inverse-bind matricy of this skin. + pub inverse_bind_matrices: Handle, + /// Additional data. + pub extras: Option, +} + +impl GltfSkin { + /// Create a skin extracting name and index from glTF def + pub fn new( + skin: &gltf::Skin, + joints: Vec>, + inverse_bind_matrices: Handle, + extras: Option, + ) -> Self { + Self { + index: skin.index(), + name: if let Some(name) = skin.name() { + name.to_string() + } else { + format!("GltfSkin{}", skin.index()) + }, + joints, + inverse_bind_matrices, + extras, + } + } + + /// Subasset label for this skin within the gLTF parent asset. + pub fn asset_label(&self) -> GltfAssetLabel { + GltfAssetLabel::Skin(self.index) + } +} + /// A glTF mesh, which may consist of multiple [`GltfPrimitives`](GltfPrimitive) /// and an optional [`GltfExtras`]. /// @@ -449,8 +518,10 @@ pub enum GltfAssetLabel { DefaultMaterial, /// `Animation{}`: glTF Animation as Bevy `AnimationClip` Animation(usize), - /// `Skin{}`: glTF mesh skin as Bevy `SkinnedMeshInverseBindposes` + /// `Skin{}`: glTF mesh skin as `GltfSkin` Skin(usize), + /// `Skin{}/InverseBindMatrices`: glTF mesh skin matrices as Bevy `SkinnedMeshInverseBindposes` + InverseBindMatrices(usize), } impl std::fmt::Display for GltfAssetLabel { @@ -480,6 +551,9 @@ impl std::fmt::Display for GltfAssetLabel { GltfAssetLabel::DefaultMaterial => f.write_str("DefaultMaterial"), GltfAssetLabel::Animation(index) => f.write_str(&format!("Animation{index}")), GltfAssetLabel::Skin(index) => f.write_str(&format!("Skin{index}")), + GltfAssetLabel::InverseBindMatrices(index) => { + f.write_str(&format!("Skin{index}/InverseBindMatrices")) + } } } } diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index eaf6425904f1aa..010e80f659c239 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -1,6 +1,6 @@ use crate::{ vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras, - GltfMeshExtras, GltfNode, GltfSceneExtras, + GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, }; #[cfg(feature = "bevy_animation")] @@ -108,7 +108,7 @@ pub enum GltfError { CircularChildren(String), /// Failed to load a file. #[error("failed to load file: {0}")] - Io(#[from] std::io::Error), + Io(#[from] Error), } /// Loads glTF files with all of their data as their corresponding bevy representations. @@ -586,42 +586,6 @@ async fn load_gltf<'a, 'b, 'c>( meshes.push(handle); } - let mut nodes_intermediate = vec![]; - let mut named_nodes_intermediate = HashMap::default(); - for node in gltf.nodes() { - nodes_intermediate.push(( - GltfNode::new( - &node, - vec![], - node.mesh() - .map(|mesh| mesh.index()) - .and_then(|i: usize| meshes.get(i).cloned()), - node_transform(&node), - get_gltf_extras(node.extras()), - ), - node.children() - .map(|child| { - ( - child.index(), - load_context - .get_label_handle(format!("{}", GltfAssetLabel::Node(node.index()))), - ) - }) - .collect::>(), - )); - if let Some(name) = node.name() { - named_nodes_intermediate.insert(name, node.index()); - } - } - let nodes = resolve_node_hierarchy(nodes_intermediate)? - .into_iter() - .map(|node| load_context.add_labeled_asset(node.asset_label().to_string(), node)) - .collect::>>(); - let named_nodes = named_nodes_intermediate - .into_iter() - .filter_map(|(name, index)| nodes.get(index).map(|handle| (name.into(), handle.clone()))) - .collect(); - let skinned_mesh_inverse_bindposes: Vec<_> = gltf .skins() .map(|gltf_skin| { @@ -633,12 +597,76 @@ async fn load_gltf<'a, 'b, 'c>( .collect(); load_context.add_labeled_asset( - skin_label(&gltf_skin), + inverse_bind_matrices_label(&gltf_skin), SkinnedMeshInverseBindposes::from(local_to_bone_bind_matrices), ) }) .collect(); + let mut nodes = HashMap::>::new(); + let mut named_nodes = HashMap::new(); + let mut skins = vec![]; + let mut named_skins = HashMap::default(); + for node in GltfTreeIterator::try_new(&gltf)? { + let skin = node.skin().map(|skin| { + let joints = skin + .joints() + .map(|joint| nodes.get(&joint.index()).unwrap().clone()) + .collect(); + + let gltf_skin = GltfSkin::new( + &skin, + joints, + skinned_mesh_inverse_bindposes[skin.index()].clone(), + get_gltf_extras(skin.extras()), + ); + + let handle = load_context.add_labeled_asset(skin_label(&skin), gltf_skin); + + skins.push(handle.clone()); + if let Some(name) = skin.name() { + named_skins.insert(name.into(), handle.clone()); + } + + handle + }); + + let children = node + .children() + .map(|child| nodes.get(&child.index()).unwrap().clone()) + .collect(); + + let mesh = node + .mesh() + .map(|mesh| mesh.index()) + .and_then(|i| meshes.get(i).cloned()); + + let gltf_node = GltfNode::new( + &node, + children, + mesh, + node_transform(&node), + skin, + get_gltf_extras(node.extras()), + ); + + #[cfg(feature = "bevy_animation")] + let gltf_node = gltf_node.with_animation_root(animation_roots.contains(&node.index())); + + let handle = load_context.add_labeled_asset(gltf_node.asset_label().to_string(), gltf_node); + nodes.insert(node.index(), handle.clone()); + if let Some(name) = node.name() { + named_nodes.insert(name.into(), handle); + } + } + + let mut nodes_to_sort = nodes.into_iter().collect::>(); + nodes_to_sort.sort_by_key(|(i, _)| *i); + let nodes = nodes_to_sort + .into_iter() + .map(|(_, resolved)| resolved) + .collect(); + let mut scenes = vec![]; let mut named_scenes = HashMap::default(); let mut active_camera_found = false; @@ -700,7 +728,6 @@ async fn load_gltf<'a, 'b, 'c>( } } - let mut warned_about_max_joints = HashSet::new(); for (&entity, &skin_index) in &entity_to_skin_index_map { let mut entity = world.entity_mut(entity); let skin = gltf.skins().nth(skin_index).unwrap(); @@ -709,16 +736,6 @@ async fn load_gltf<'a, 'b, 'c>( .map(|node| node_index_to_entity_map[&node.index()]) .collect(); - if joint_entities.len() > MAX_JOINTS && warned_about_max_joints.insert(skin_index) { - warn!( - "The glTF skin {:?} has {} joints, but the maximum supported is {}", - skin.name() - .map(ToString::to_string) - .unwrap_or_else(|| skin.index().to_string()), - joint_entities.len(), - MAX_JOINTS - ); - } entity.insert(SkinnedMesh { inverse_bindposes: skinned_mesh_inverse_bindposes[skin_index].clone(), joints: joint_entities, @@ -742,6 +759,8 @@ async fn load_gltf<'a, 'b, 'c>( named_scenes, meshes, named_meshes, + skins, + named_skins, materials, named_materials, nodes, @@ -758,7 +777,7 @@ async fn load_gltf<'a, 'b, 'c>( }) } -fn get_gltf_extras(extras: &gltf::json::Extras) -> Option { +fn get_gltf_extras(extras: &json::Extras) -> Option { extras.as_ref().map(|extras| GltfExtras { value: extras.get().to_string(), }) @@ -780,9 +799,9 @@ fn node_transform(node: &Node) -> Transform { rotation, scale, } => Transform { - translation: bevy_math::Vec3::from(translation), + translation: Vec3::from(translation), rotation: bevy_math::Quat::from_array(rotation), - scale: bevy_math::Vec3::from(scale), + scale: Vec3::from(scale), }, } } @@ -830,7 +849,7 @@ async fn load_image<'a, 'b>( .name() .map_or("Unknown GLTF Texture".to_string(), ToString::to_string); match gltf_texture.source().source() { - gltf::image::Source::View { view, mime_type } => { + Source::View { view, mime_type } => { let start = view.offset(); let end = view.offset() + view.length(); let buffer = &buffer_data[view.buffer().index()][start..end]; @@ -849,7 +868,7 @@ async fn load_image<'a, 'b>( label: GltfAssetLabel::Texture(gltf_texture.index()), }) } - gltf::image::Source::Uri { uri, mime_type } => { + Source::Uri { uri, mime_type } => { let uri = percent_encoding::percent_decode_str(uri) .decode_utf8() .unwrap(); @@ -1547,10 +1566,16 @@ fn scene_label(scene: &gltf::Scene) -> String { GltfAssetLabel::Scene(scene.index()).to_string() } +/// Return the label for the `skin`. fn skin_label(skin: &gltf::Skin) -> String { GltfAssetLabel::Skin(skin.index()).to_string() } +/// Return the label for the `inverseBindMatrices` of the node. +fn inverse_bind_matrices_label(skin: &gltf::Skin) -> String { + GltfAssetLabel::InverseBindMatrices(skin.index()).to_string() +} + /// Extracts the texture sampler data from the glTF texture. fn texture_sampler(texture: &gltf::Texture) -> ImageSamplerDescriptor { let gltf_sampler = texture.sampler(); @@ -1667,57 +1692,109 @@ async fn load_buffers( Ok(buffer_data) } -#[allow(clippy::result_large_err)] -fn resolve_node_hierarchy( - nodes_intermediate: Vec<(GltfNode, Vec<(usize, Handle)>)>, -) -> Result, GltfError> { - let mut empty_children = VecDeque::new(); - let mut parents = vec![None; nodes_intermediate.len()]; - let mut unprocessed_nodes = nodes_intermediate - .into_iter() - .enumerate() - .map(|(i, (node, children))| { - for (child_index, _child_handle) in &children { - let parent = parents.get_mut(*child_index).unwrap(); - *parent = Some(i); - } - let children = children.into_iter().collect::>(); - if children.is_empty() { - empty_children.push_back(i); +/// Iterator for a Gltf tree. +/// +/// It resolves a Gltf tree and allows for a safe Gltf nodes iteration, +/// putting dependant nodes before dependencies. +struct GltfTreeIterator<'a> { + nodes: Vec>, +} + +impl<'a> GltfTreeIterator<'a> { + #[allow(clippy::result_large_err)] + fn try_new(gltf: &'a gltf::Gltf) -> Result { + let nodes = gltf.nodes().collect::>(); + + let mut empty_children = VecDeque::new(); + let mut parents = vec![None; nodes.len()]; + let mut unprocessed_nodes = nodes + .into_iter() + .enumerate() + .map(|(i, node)| { + let children = node + .children() + .map(|child| child.index()) + .collect::>(); + for &child in &children { + let parent = parents.get_mut(child).unwrap(); + *parent = Some(i); + } + if children.is_empty() { + empty_children.push_back(i); + } + (i, (node, children)) + }) + .collect::>(); + + let mut nodes = Vec::new(); + let mut warned_about_max_joints = HashSet::new(); + while let Some(index) = empty_children.pop_front() { + if let Some(skin) = unprocessed_nodes.get(&index).unwrap().0.skin() { + if skin.joints().len() > MAX_JOINTS && warned_about_max_joints.insert(skin.index()) + { + warn!( + "The glTF skin {:?} has {} joints, but the maximum supported is {}", + skin.name() + .map(ToString::to_string) + .unwrap_or_else(|| skin.index().to_string()), + skin.joints().len(), + MAX_JOINTS + ); + } + + let skin_has_dependencies = skin + .joints() + .any(|joint| unprocessed_nodes.contains_key(&joint.index())); + + if skin_has_dependencies && unprocessed_nodes.len() != 1 { + empty_children.push_back(index); + continue; + } } - (i, (node, children)) - }) - .collect::>(); - let mut nodes = std::collections::HashMap::::new(); - while let Some(index) = empty_children.pop_front() { - let (node, children) = unprocessed_nodes.remove(&index).unwrap(); - assert!(children.is_empty()); - nodes.insert(index, node); - if let Some(parent_index) = parents[index] { - let (parent_node, parent_children) = unprocessed_nodes.get_mut(&parent_index).unwrap(); - - let handle = parent_children.remove(&index).unwrap(); - parent_node.children.push(handle); - if parent_children.is_empty() { - empty_children.push_back(parent_index); + + let (node, children) = unprocessed_nodes.remove(&index).unwrap(); + assert!(children.is_empty()); + nodes.push(node); + + if let Some(parent_index) = parents[index] { + let (_, parent_children) = unprocessed_nodes.get_mut(&parent_index).unwrap(); + + assert!(parent_children.remove(&index)); + if parent_children.is_empty() { + empty_children.push_back(parent_index); + } } } + + if !unprocessed_nodes.is_empty() { + return Err(GltfError::CircularChildren(format!( + "{:?}", + unprocessed_nodes + .iter() + .map(|(k, _v)| *k) + .collect::>(), + ))); + } + + nodes.reverse(); + Ok(Self { + nodes: nodes.into_iter().collect(), + }) } - if !unprocessed_nodes.is_empty() { - return Err(GltfError::CircularChildren(format!( - "{:?}", - unprocessed_nodes - .iter() - .map(|(k, _v)| *k) - .collect::>(), - ))); +} + +impl<'a> Iterator for GltfTreeIterator<'a> { + type Item = Node<'a>; + + fn next(&mut self) -> Option { + self.nodes.pop() + } +} + +impl<'a> ExactSizeIterator for GltfTreeIterator<'a> { + fn len(&self) -> usize { + self.nodes.len() } - let mut nodes_to_sort = nodes.into_iter().collect::>(); - nodes_to_sort.sort_by_key(|(i, _)| *i); - Ok(nodes_to_sort - .into_iter() - .map(|(_, resolved)| resolved) - .collect()) } enum ImageOrPath { @@ -2003,7 +2080,7 @@ fn material_needs_tangents(material: &Material) -> bool { mod test { use std::path::Path; - use crate::{Gltf, GltfAssetLabel, GltfNode}; + use crate::{Gltf, GltfAssetLabel, GltfNode, GltfSkin}; use bevy_app::App; use bevy_asset::{ io::{ @@ -2015,6 +2092,7 @@ mod test { use bevy_core::TaskPoolPlugin; use bevy_ecs::world::World; use bevy_log::LogPlugin; + use bevy_render::mesh::{skinning::SkinnedMeshInverseBindposes, MeshPlugin}; use bevy_scene::ScenePlugin; fn test_app(dir: Dir) -> App { @@ -2029,6 +2107,7 @@ mod test { TaskPoolPlugin::default(), AssetPlugin::default(), ScenePlugin, + MeshPlugin, crate::GltfPlugin::default(), )); @@ -2063,10 +2142,10 @@ mod test { app.update(); run_app_until(&mut app, |_world| { let load_state = asset_server.get_load_state(handle_id).unwrap(); - if load_state == LoadState::Loaded { - Some(()) - } else { - None + match load_state { + LoadState::Loaded => Some(()), + LoadState::Failed(err) => panic!("{err}"), + _ => None, } }); app @@ -2347,4 +2426,83 @@ mod test { let load_state = asset_server.get_load_state(handle_id).unwrap(); assert!(matches!(load_state, LoadState::Failed(_))); } + + #[test] + fn skin_node() { + let gltf_path = "test.gltf"; + let app = load_gltf_into_app( + gltf_path, + r#" +{ + "asset": { + "version": "2.0" + }, + "nodes": [ + { + "name": "skinned", + "skin": 0, + "children": [1, 2] + }, + { + "name": "joint1" + }, + { + "name": "joint2" + } + ], + "skins": [ + { + "inverseBindMatrices": 0, + "joints": [1, 2] + } + ], + "buffers": [ + { + "uri" : "data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=", + "byteLength" : 128 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 128 + } + ], + "accessors": [ + { + "bufferView" : 0, + "componentType" : 5126, + "count" : 2, + "type" : "MAT4" + } + ], + "scene": 0, + "scenes": [{ "nodes": [0] }] +} +"#, + ); + let asset_server = app.world().resource::(); + let handle = asset_server.load(gltf_path); + let gltf_root_assets = app.world().resource::>(); + let gltf_node_assets = app.world().resource::>(); + let gltf_skin_assets = app.world().resource::>(); + let gltf_inverse_bind_matrices = app + .world() + .resource::>(); + let gltf_root = gltf_root_assets.get(&handle).unwrap(); + + assert_eq!(gltf_root.skins.len(), 1); + assert_eq!(gltf_root.nodes.len(), 3); + + let skin = gltf_skin_assets.get(&gltf_root.skins[0]).unwrap(); + assert_eq!(skin.joints.len(), 2); + assert_eq!(skin.joints[0], gltf_root.nodes[1]); + assert_eq!(skin.joints[1], gltf_root.nodes[2]); + assert!(gltf_inverse_bind_matrices.contains(&skin.inverse_bind_matrices)); + + let skinned_node = gltf_node_assets.get(&gltf_root.nodes[0]).unwrap(); + assert_eq!(skinned_node.name, "skinned"); + assert_eq!(skinned_node.children.len(), 2); + assert_eq!(skinned_node.skin.as_ref(), Some(&gltf_root.skins[0])); + } } diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index 7657cb54a3106b..d42ecbf3977710 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -272,7 +272,7 @@ pub(crate) fn convert_attribute( } gltf::Semantic::Extras(name) => custom_vertex_attributes .get(name.as_str()) - .map(|attr| (attr.clone(), ConversionMode::Any)), + .map(|attr| (*attr, ConversionMode::Any)), _ => None, } { let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data); diff --git a/crates/bevy_hierarchy/Cargo.toml b/crates/bevy_hierarchy/Cargo.toml index 134dbe64d6a4a9..7e696bae125c57 100644 --- a/crates/bevy_hierarchy/Cargo.toml +++ b/crates/bevy_hierarchy/Cargo.toml @@ -31,5 +31,5 @@ smallvec = { version = "1.11", features = ["union", "const_generics"] } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index ba772d328d554b..6b949119cd111d 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -339,8 +339,19 @@ pub trait BuildChildren { type Builder<'a>: ChildBuild; /// Takes a closure which builds children for this entity using [`ChildBuild`]. + /// + /// For convenient spawning of a single child, you can use [`with_child`]. + /// + /// [`with_child`]: BuildChildren::with_child fn with_children(&mut self, f: impl FnOnce(&mut Self::Builder<'_>)) -> &mut Self; + /// Spawns the passed bundle and adds it to this entity as a child. + /// + /// For efficient spawning of multiple children, use [`with_children`]. + /// + /// [`with_children`]: BuildChildren::with_children + fn with_child(&mut self, bundle: B) -> &mut Self; + /// Pushes children to the back of the builder's children. For any entities that are /// already a child of this one, this method does nothing. /// @@ -432,6 +443,13 @@ impl BuildChildren for EntityCommands<'_> { self } + fn with_child(&mut self, bundle: B) -> &mut Self { + let parent = self.id(); + let child = self.commands().spawn(bundle).id(); + self.commands().add(PushChild { parent, child }); + self + } + fn push_children(&mut self, children: &[Entity]) -> &mut Self { let parent = self.id(); if children.contains(&parent) { @@ -566,6 +584,17 @@ impl BuildChildren for EntityWorldMut<'_> { self } + fn with_child(&mut self, bundle: B) -> &mut Self { + let child = self.world_scope(|world| world.spawn(bundle).id()); + if let Some(mut children_component) = self.get_mut::() { + children_component.0.retain(|value| child != *value); + children_component.0.push(child); + } else { + self.insert(Children::from_entities(&[child])); + } + self + } + fn add_child(&mut self, child: Entity) -> &mut Self { let parent = self.id(); if child == parent { @@ -692,6 +721,14 @@ mod tests { assert_eq!(world.get::(parent).map(|c| &**c), children); } + /// Assert the number of children in the parent's [`Children`] component if it exists. + fn assert_num_children(world: &World, parent: Entity, num_children: usize) { + assert_eq!( + world.get::(parent).map(|c| c.len()).unwrap_or(0), + num_children + ); + } + /// Used to omit a number of events that are not relevant to a particular test. fn omit_events(world: &mut World, number: usize) { let mut events_resource = world.resource_mut::>(); @@ -859,6 +896,19 @@ mod tests { assert_eq!(*world.get::(children[1]).unwrap(), Parent(parent)); } + #[test] + fn build_child() { + let mut world = World::default(); + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, &world); + + let parent = commands.spawn(C(1)).id(); + commands.entity(parent).with_child(C(2)); + + queue.apply(&mut world); + assert_eq!(world.get::(parent).unwrap().0.len(), 1); + } + #[test] fn push_and_insert_and_remove_children_commands() { let mut world = World::default(); @@ -1228,4 +1278,23 @@ mod tests { let children = query.get(&world, parent); assert!(children.is_err()); } + + #[test] + fn with_child() { + let world = &mut World::new(); + world.insert_resource(Events::::default()); + + let a = world.spawn_empty().id(); + let b = (); + let c = (); + let d = (); + + world.entity_mut(a).with_child(b); + + assert_num_children(world, a, 1); + + world.entity_mut(a).with_child(c).with_child(d); + + assert_num_children(world, a, 3); + } } diff --git a/crates/bevy_hierarchy/src/components/parent.rs b/crates/bevy_hierarchy/src/components/parent.rs index bb2b6abba09b1e..99d8e45460a8d8 100644 --- a/crates/bevy_hierarchy/src/components/parent.rs +++ b/crates/bevy_hierarchy/src/components/parent.rs @@ -3,6 +3,7 @@ use bevy_ecs::reflect::{ReflectComponent, ReflectMapEntities}; use bevy_ecs::{ component::Component, entity::{Entity, EntityMapper, MapEntities}, + traversal::Traversal, world::{FromWorld, World}, }; use std::ops::Deref; @@ -69,3 +70,14 @@ impl Deref for Parent { &self.0 } } + +/// This provides generalized hierarchy traversal for use in [event propagation]. +/// +/// `Parent::traverse` will never form loops in properly-constructed hierarchies. +/// +/// [event propagation]: bevy_ecs::observer::Trigger::propagate +impl Traversal for Parent { + fn traverse(&self) -> Option { + Some(self.0) + } +} diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index 2cfeaf4cc088b1..9abf8db2999a36 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -43,5 +43,5 @@ smol_str = "0.2" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index 58db560f6de8fc..0a9109e46bdb6f 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -88,8 +88,12 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// The event is consumed inside of the [`keyboard_input_system`] /// to update the [`ButtonInput`](ButtonInput) resource. -#[derive(Event, Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[derive(Event, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Hash) +)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( all(feature = "serialize", feature = "bevy_reflect"), @@ -102,6 +106,10 @@ pub struct KeyboardInput { pub logical_key: Key, /// The press state of the key. pub state: ButtonState, + /// On some systems, holding down a key for some period of time causes that key to be repeated + /// as though it were being pressed and released repeatedly. This field is [`true`] if this + /// event is the result of one of those repeats. + pub repeat: bool, /// Window that received the input. pub window: Entity, } @@ -832,7 +840,7 @@ pub enum Key { /// Scroll up or display previous page of content. PageUp, /// Used to remove the character to the left of the cursor. This key value is also used for - /// the key labeled `Delete` on MacOS keyboards. + /// the key labeled `Delete` on macOS keyboards. Backspace, /// Remove the currently selected input. Clear, @@ -843,7 +851,7 @@ pub enum Key { /// Cut the current selection. (`APPCOMMAND_CUT`) Cut, /// Used to delete the character to the right of the cursor. This key value is also used for the - /// key labeled `Delete` on MacOS keyboards when `Fn` is active. + /// key labeled `Delete` on macOS keyboards when `Fn` is active. Delete, /// The Erase to End of Field key. This key deletes all characters from the current cursor /// position to the end of the current field. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 821350c24c1c37..7505c8f0be3691 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -152,7 +152,11 @@ accesskit_unix = ["bevy_winit/accesskit_unix"] bevy_text = ["dep:bevy_text", "bevy_ui?/bevy_text"] -bevy_render = ["dep:bevy_render", "bevy_scene?/bevy_render"] +bevy_render = [ + "dep:bevy_render", + "bevy_scene?/bevy_render", + "bevy_gizmos?/bevy_render", +] # Enable assertions to check the validity of parameters passed to glam glam_assert = ["bevy_math/glam_assert"] @@ -187,7 +191,7 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"] bevy_dev_tools = ["dep:bevy_dev_tools"] # Provides a picking functionality -bevy_picking = ["dep:bevy_picking"] +bevy_picking = ["dep:bevy_picking", "bevy_ui?/bevy_picking"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] @@ -195,6 +199,16 @@ ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] # Enable built in global state machines bevy_state = ["dep:bevy_state"] +# Enables source location tracking for change detection, which can assist with debugging +track_change_detection = ["bevy_ecs/track_change_detection"] + +# Enable function reflection +reflect_functions = [ + "bevy_reflect/functions", + "bevy_app/reflect_functions", + "bevy_ecs/reflect_functions", +] + [dependencies] # bevy bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev" } @@ -224,7 +238,6 @@ bevy_audio = { path = "../bevy_audio", optional = true, version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", optional = true, version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.15.0-dev" } bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.15.0-dev" } -bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, version = "0.15.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.15.0-dev" } bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.15.0-dev", default-features = false } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" } @@ -241,5 +254,5 @@ bevy_winit = { path = "../bevy_winit", optional = true, version = "0.15.0-dev" } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 88f92f38e74568..a84bbdb5735906 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -1,171 +1,80 @@ -use bevy_app::{Plugin, PluginGroup, PluginGroupBuilder}; - -/// This plugin group will add all the default plugins for a *Bevy* application: -/// * [`PanicHandlerPlugin`](crate::app::PanicHandlerPlugin) -/// * [`LogPlugin`](crate::log::LogPlugin) -/// * [`TaskPoolPlugin`](crate::core::TaskPoolPlugin) -/// * [`TypeRegistrationPlugin`](crate::core::TypeRegistrationPlugin) -/// * [`FrameCountPlugin`](crate::core::FrameCountPlugin) -/// * [`TimePlugin`](crate::time::TimePlugin) -/// * [`TransformPlugin`](crate::transform::TransformPlugin) -/// * [`HierarchyPlugin`](crate::hierarchy::HierarchyPlugin) -/// * [`DiagnosticsPlugin`](crate::diagnostic::DiagnosticsPlugin) -/// * [`InputPlugin`](crate::input::InputPlugin) -/// * [`WindowPlugin`](crate::window::WindowPlugin) -/// * [`AccessibilityPlugin`](crate::a11y::AccessibilityPlugin) -/// * [`AssetPlugin`](crate::asset::AssetPlugin) - with feature `bevy_asset` -/// * [`ScenePlugin`](crate::scene::ScenePlugin) - with feature `bevy_scene` -/// * [`WinitPlugin`](crate::winit::WinitPlugin) - with feature `bevy_winit` -/// * [`RenderPlugin`](crate::render::RenderPlugin) - with feature `bevy_render` -/// * [`ImagePlugin`](crate::render::texture::ImagePlugin) - with feature `bevy_render` -/// * [`PipelinedRenderingPlugin`](crate::render::pipelined_rendering::PipelinedRenderingPlugin) - with feature `bevy_render` when not targeting `wasm32` -/// * [`CorePipelinePlugin`](crate::core_pipeline::CorePipelinePlugin) - with feature `bevy_core_pipeline` -/// * [`SpritePlugin`](crate::sprite::SpritePlugin) - with feature `bevy_sprite` -/// * [`TextPlugin`](crate::text::TextPlugin) - with feature `bevy_text` -/// * [`UiPlugin`](crate::ui::UiPlugin) - with feature `bevy_ui` -/// * [`PbrPlugin`](crate::pbr::PbrPlugin) - with feature `bevy_pbr` -/// * [`GltfPlugin`](crate::gltf::GltfPlugin) - with feature `bevy_gltf` -/// * [`AudioPlugin`](crate::audio::AudioPlugin) - with feature `bevy_audio` -/// * [`GilrsPlugin`](crate::gilrs::GilrsPlugin) - with feature `bevy_gilrs` -/// * [`AnimationPlugin`](crate::animation::AnimationPlugin) - with feature `bevy_animation` -/// * [`GizmoPlugin`](crate::gizmos::GizmoPlugin) - with feature `bevy_gizmos` -/// * [`StatesPlugin`](crate::state::app::StatesPlugin) - with feature `bevy_state` -/// * [`DevToolsPlugin`](crate::dev_tools::DevToolsPlugin) - with feature `bevy_dev_tools` -/// * [`CiTestingPlugin`](crate::dev_tools::ci_testing::CiTestingPlugin) - with feature `bevy_ci_testing` -/// -/// [`DefaultPlugins`] obeys *Cargo* *feature* flags. Users may exert control over this plugin group -/// by disabling `default-features` in their `Cargo.toml` and enabling only those features -/// that they wish to use. -/// -/// [`DefaultPlugins`] contains all the plugins typically required to build -/// a *Bevy* application which includes a *window* and presentation components. -/// For *headless* cases – without a *window* or presentation, see [`MinimalPlugins`]. -pub struct DefaultPlugins; - -impl PluginGroup for DefaultPlugins { - fn build(self) -> PluginGroupBuilder { - let mut group = PluginGroupBuilder::start::(); - group = group - .add(bevy_app::PanicHandlerPlugin) - .add(bevy_log::LogPlugin::default()) - .add(bevy_core::TaskPoolPlugin::default()) - .add(bevy_core::TypeRegistrationPlugin) - .add(bevy_core::FrameCountPlugin) - .add(bevy_time::TimePlugin) - .add(bevy_transform::TransformPlugin) - .add(bevy_hierarchy::HierarchyPlugin) - .add(bevy_diagnostic::DiagnosticsPlugin) - .add(bevy_input::InputPlugin) - .add(bevy_window::WindowPlugin::default()) - .add(bevy_a11y::AccessibilityPlugin); - - #[cfg(not(target_arch = "wasm32"))] - { - group = group.add(bevy_app::TerminalCtrlCHandlerPlugin); - } - +use bevy_app::{plugin_group, Plugin}; + +plugin_group! { + /// This plugin group will add all the default plugins for a *Bevy* application: + pub struct DefaultPlugins { + bevy_app:::PanicHandlerPlugin, + bevy_log:::LogPlugin, + bevy_core:::TaskPoolPlugin, + bevy_core:::TypeRegistrationPlugin, + bevy_core:::FrameCountPlugin, + bevy_time:::TimePlugin, + bevy_transform:::TransformPlugin, + bevy_hierarchy:::HierarchyPlugin, + bevy_diagnostic:::DiagnosticsPlugin, + bevy_input:::InputPlugin, + bevy_window:::WindowPlugin, + bevy_a11y:::AccessibilityPlugin, + #[custom(cfg(not(target_arch = "wasm32")))] + bevy_app:::TerminalCtrlCHandlerPlugin, #[cfg(feature = "bevy_asset")] - { - group = group.add(bevy_asset::AssetPlugin::default()); - } - + bevy_asset:::AssetPlugin, #[cfg(feature = "bevy_scene")] - { - group = group.add(bevy_scene::ScenePlugin); - } - + bevy_scene:::ScenePlugin, #[cfg(feature = "bevy_winit")] - { - group = group.add::(bevy_winit::WinitPlugin::default()); - } - + bevy_winit:::WinitPlugin, #[cfg(feature = "bevy_render")] - { - group = group - .add(bevy_render::RenderPlugin::default()) - // NOTE: Load this after renderer initialization so that it knows about the supported - // compressed texture formats - .add(bevy_render::texture::ImagePlugin::default()); - - #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - { - group = group.add(bevy_render::pipelined_rendering::PipelinedRenderingPlugin); - } - } - + bevy_render:::RenderPlugin, + // NOTE: Load this after renderer initialization so that it knows about the supported + // compressed texture formats. + #[cfg(feature = "bevy_render")] + bevy_render::texture:::ImagePlugin, + #[cfg(feature = "bevy_render")] + #[custom(cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded")))] + bevy_render::pipelined_rendering:::PipelinedRenderingPlugin, #[cfg(feature = "bevy_core_pipeline")] - { - group = group.add(bevy_core_pipeline::CorePipelinePlugin); - } - + bevy_core_pipeline:::CorePipelinePlugin, #[cfg(feature = "bevy_sprite")] - { - group = group.add(bevy_sprite::SpritePlugin); - } - + bevy_sprite:::SpritePlugin, #[cfg(feature = "bevy_text")] - { - group = group.add(bevy_text::TextPlugin); - } - + bevy_text:::TextPlugin, #[cfg(feature = "bevy_ui")] - { - group = group.add(bevy_ui::UiPlugin); - } - + bevy_ui:::UiPlugin, #[cfg(feature = "bevy_pbr")] - { - group = group.add(bevy_pbr::PbrPlugin::default()); - } - + bevy_pbr:::PbrPlugin, // NOTE: Load this after renderer initialization so that it knows about the supported - // compressed texture formats + // compressed texture formats. #[cfg(feature = "bevy_gltf")] - { - group = group.add(bevy_gltf::GltfPlugin::default()); - } - + bevy_gltf:::GltfPlugin, #[cfg(feature = "bevy_audio")] - { - group = group.add(bevy_audio::AudioPlugin::default()); - } - + bevy_audio:::AudioPlugin, #[cfg(feature = "bevy_gilrs")] - { - group = group.add(bevy_gilrs::GilrsPlugin); - } - + bevy_gilrs:::GilrsPlugin, #[cfg(feature = "bevy_animation")] - { - group = group.add(bevy_animation::AnimationPlugin); - } - + bevy_animation:::AnimationPlugin, #[cfg(feature = "bevy_gizmos")] - { - group = group.add(bevy_gizmos::GizmoPlugin); - } - + bevy_gizmos:::GizmoPlugin, #[cfg(feature = "bevy_state")] - { - group = group.add(bevy_state::app::StatesPlugin); - } - + bevy_state::app:::StatesPlugin, + #[cfg(feature = "bevy_picking")] + bevy_picking:::DefaultPickingPlugins, #[cfg(feature = "bevy_dev_tools")] - { - group = group.add(bevy_dev_tools::DevToolsPlugin); - } - + bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] - { - group = group.add(bevy_dev_tools::ci_testing::CiTestingPlugin); - } - - group = group.add(IgnoreAmbiguitiesPlugin); - - group + bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[doc(hidden)] + :IgnoreAmbiguitiesPlugin, } + /// [`DefaultPlugins`] obeys *Cargo* *feature* flags. Users may exert control over this plugin group + /// by disabling `default-features` in their `Cargo.toml` and enabling only those features + /// that they wish to use. + /// + /// [`DefaultPlugins`] contains all the plugins typically required to build + /// a *Bevy* application which includes a *window* and presentation components. + /// For *headless* cases – without a *window* or presentation, see [`MinimalPlugins`]. } +#[derive(Default)] struct IgnoreAmbiguitiesPlugin; impl Plugin for IgnoreAmbiguitiesPlugin { @@ -185,39 +94,23 @@ impl Plugin for IgnoreAmbiguitiesPlugin { } } -/// This plugin group will add the minimal plugins for a *Bevy* application: -/// * [`TaskPoolPlugin`](crate::core::TaskPoolPlugin) -/// * [`TypeRegistrationPlugin`](crate::core::TypeRegistrationPlugin) -/// * [`FrameCountPlugin`](crate::core::FrameCountPlugin) -/// * [`TimePlugin`](crate::time::TimePlugin) -/// * [`ScheduleRunnerPlugin`](crate::app::ScheduleRunnerPlugin) -/// * [`CiTestingPlugin`](crate::dev_tools::ci_testing::CiTestingPlugin) - with feature `bevy_ci_testing` -/// -/// This group of plugins is intended for use for minimal, *headless* programs – -/// see the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs) -/// – and includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) -/// to provide functionality that would otherwise be driven by a windowed application's -/// *event loop* or *message loop*. -/// -/// Windowed applications that wish to use a reduced set of plugins should consider the -/// [`DefaultPlugins`] plugin group which can be controlled with *Cargo* *feature* flags. -pub struct MinimalPlugins; - -impl PluginGroup for MinimalPlugins { - fn build(self) -> PluginGroupBuilder { - let mut group = PluginGroupBuilder::start::(); - group = group - .add(bevy_core::TaskPoolPlugin::default()) - .add(bevy_core::TypeRegistrationPlugin) - .add(bevy_core::FrameCountPlugin) - .add(bevy_time::TimePlugin) - .add(bevy_app::ScheduleRunnerPlugin::default()); - +plugin_group! { + /// This plugin group will add the minimal plugins for a *Bevy* application: + pub struct MinimalPlugins { + bevy_core:::TaskPoolPlugin, + bevy_core:::TypeRegistrationPlugin, + bevy_core:::FrameCountPlugin, + bevy_time:::TimePlugin, + bevy_app:::ScheduleRunnerPlugin, #[cfg(feature = "bevy_ci_testing")] - { - group = group.add(bevy_dev_tools::ci_testing::CiTestingPlugin); - } - - group + bevy_dev_tools::ci_testing:::CiTestingPlugin, } + /// This group of plugins is intended for use for minimal, *headless* programs – + /// see the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs) + /// – and includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) + /// to provide functionality that would otherwise be driven by a windowed application's + /// *event loop* or *message loop*. + /// + /// Windowed applications that wish to use a reduced set of plugins should consider the + /// [`DefaultPlugins`] plugin group which can be controlled with *Cargo* *feature* flags. } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 1f03cf09dc29d7..bc71e57efa5197 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -29,8 +29,6 @@ pub use bevy_core_pipeline as core_pipeline; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; -#[cfg(feature = "bevy_dynamic_plugin")] -pub use bevy_dynamic_plugin as dynamic_plugin; pub use bevy_ecs as ecs; #[cfg(feature = "bevy_gilrs")] pub use bevy_gilrs as gilrs; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 70881a3d39a11e..812a674ed93eea 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -51,10 +51,6 @@ pub use crate::text::prelude::*; #[cfg(feature = "bevy_ui")] pub use crate::ui::prelude::*; -#[doc(hidden)] -#[cfg(feature = "bevy_dynamic_plugin")] -pub use crate::dynamic_plugin::*; - #[doc(hidden)] #[cfg(feature = "bevy_gizmos")] pub use crate::gizmos::prelude::*; @@ -70,3 +66,7 @@ pub use crate::state::prelude::*; #[doc(hidden)] #[cfg(feature = "bevy_gltf")] pub use crate::gltf::prelude::*; + +#[doc(hidden)] +#[cfg(feature = "bevy_picking")] +pub use crate::picking::prelude::*; diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 0dd47920af26a9..d996a782210450 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -40,5 +40,5 @@ tracing-wasm = "0.2.1" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index f1febadba6d7da..c2fdd1639b83df 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -5,7 +5,7 @@ )] //! This crate provides logging functions and configuration for [Bevy](https://bevyengine.org) -//! apps, and automatically configures platform specific log handlers (i.e. WASM or Android). +//! apps, and automatically configures platform specific log handlers (i.e. Wasm or Android). //! //! The macros provided for logging are reexported from [`tracing`](https://docs.rs/tracing), //! and behave identically to it. @@ -75,7 +75,7 @@ pub(crate) struct FlushGuard(SyncCell); /// logging to `stdout`. /// * Using [`android_log-sys`](https://crates.io/crates/android_log-sys) on Android, /// logging to Android logs. -/// * Using [`tracing-wasm`](https://crates.io/crates/tracing-wasm) in WASM, logging +/// * Using [`tracing-wasm`](https://crates.io/crates/tracing-wasm) in Wasm, logging /// to the browser console. /// /// You can configure this plugin. diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index f8d739a07c3937..4b135f205e4f23 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -20,5 +20,5 @@ proc-macro2 = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index eebd683cfc7d96..d158075608648b 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -10,8 +10,9 @@ keywords = ["bevy"] rust-version = "1.68.2" [dependencies] -glam = { version = "0.27", features = ["bytemuck"] } +glam = { version = "0.28", features = ["bytemuck"] } thiserror = "1.0" +itertools = "0.13.0" serde = { version = "1", features = ["derive"], optional = true } libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } @@ -32,8 +33,7 @@ rand = "0.8" rand_chacha = "0.3" # Enable the approx feature when testing. bevy_math = { path = ".", version = "0.15.0-dev", features = ["approx"] } -glam = { version = "0.27", features = ["approx"] } - +glam = { version = "0.28", features = ["approx"] } [features] default = ["rand", "bevy_reflect"] @@ -56,5 +56,5 @@ rand = ["dep:rand", "dep:rand_distr", "glam/rand"] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_math/clippy.toml b/crates/bevy_math/clippy.toml new file mode 100644 index 00000000000000..bad8801c01736a --- /dev/null +++ b/crates/bevy_math/clippy.toml @@ -0,0 +1,29 @@ +disallowed-methods = [ + { path = "f32::powi", reason = "use ops::FloatPow::squared, ops::FloatPow::cubed, or ops::powf instead for libm determinism" }, + { path = "f32::log", reason = "use ops::ln, ops::log2, or ops::log10 instead for libm determinism" }, + { path = "f32::abs_sub", reason = "deprecated and deeply confusing method" }, + { path = "f32::powf", reason = "use ops::powf instead for libm determinism" }, + { path = "f32::exp", reason = "use ops::exp instead for libm determinism" }, + { path = "f32::exp2", reason = "use ops::exp2 instead for libm determinism" }, + { path = "f32::ln", reason = "use ops::ln instead for libm determinism" }, + { path = "f32::log2", reason = "use ops::log2 instead for libm determinism" }, + { path = "f32::log10", reason = "use ops::log10 instead for libm determinism" }, + { path = "f32::cbrt", reason = "use ops::cbrt instead for libm determinism" }, + { path = "f32::hypot", reason = "use ops::hypot instead for libm determinism" }, + { path = "f32::sin", reason = "use ops::sin instead for libm determinism" }, + { path = "f32::cos", reason = "use ops::cos instead for libm determinism" }, + { path = "f32::tan", reason = "use ops::tan instead for libm determinism" }, + { path = "f32::asin", reason = "use ops::asin instead for libm determinism" }, + { path = "f32::acos", reason = "use ops::acos instead for libm determinism" }, + { path = "f32::atan", reason = "use ops::atan instead for libm determinism" }, + { path = "f32::atan2", reason = "use ops::atan2 instead for libm determinism" }, + { path = "f32::sin_cos", reason = "use ops::sin_cos instead for libm determinism" }, + { path = "f32::exp_m1", reason = "use ops::exp_m1 instead for libm determinism" }, + { path = "f32::ln_1p", reason = "use ops::ln_1p instead for libm determinism" }, + { path = "f32::sinh", reason = "use ops::sinh instead for libm determinism" }, + { path = "f32::cosh", reason = "use ops::cosh instead for libm determinism" }, + { path = "f32::tanh", reason = "use ops::tanh instead for libm determinism" }, + { path = "f32::asinh", reason = "use ops::asinh instead for libm determinism" }, + { path = "f32::acosh", reason = "use ops::acosh instead for libm determinism" }, + { path = "f32::atanh", reason = "use ops::atanh instead for libm determinism" }, +] diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 87123075a90b0a..1fdf6c75b0ca7c 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -1,7 +1,11 @@ mod primitive_impls; use super::{BoundingVolume, IntersectsVolume}; -use crate::prelude::{Mat2, Rot2, Vec2}; +use crate::{ + ops::FloatPow, + prelude::{Mat2, Rot2, Vec2}, + Isometry2d, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -18,14 +22,12 @@ fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 { points.iter().fold(Vec2::ZERO, |acc, point| acc + *point) * denom } -/// A trait with methods that return 2D bounded volumes for a shape +/// A trait with methods that return 2D bounding 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: impl Into) -> 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: impl Into) -> BoundingCircle; + /// Get an axis-aligned bounding box for the shape translated and rotated by the given isometry. + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d; + /// Get a bounding circle for the shape translated and rotated by the given isometry. + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle; } /// A 2D axis-aligned bounding box, or bounding rectangle @@ -51,20 +53,15 @@ impl Aabb2d { } /// Computes the smallest [`Aabb2d`] containing the given set of points, - /// transformed by `translation` and `rotation`. + /// transformed by the rotation and translation of the given isometry. /// /// # Panics /// /// Panics if the given set of points is empty. #[inline(always)] - pub fn from_point_cloud( - translation: Vec2, - rotation: impl Into, - points: &[Vec2], - ) -> Aabb2d { + pub fn from_point_cloud(isometry: Isometry2d, points: &[Vec2]) -> Aabb2d { // Transform all points by rotation - let rotation: Rot2 = rotation.into(); - let mut iter = points.iter().map(|point| rotation * *point); + let mut iter = points.iter().map(|point| isometry.rotation * *point); let first = iter .next() @@ -75,8 +72,8 @@ impl Aabb2d { }); Aabb2d { - min: min + translation, - max: max + translation, + min: min + isometry.translation, + max: max + isometry.translation, } } @@ -255,7 +252,7 @@ impl IntersectsVolume for Aabb2d { fn intersects(&self, circle: &BoundingCircle) -> bool { let closest_point = self.closest_point(circle.center); let distance_squared = circle.center.distance_squared(closest_point); - let radius_squared = circle.radius().powi(2); + let radius_squared = circle.radius().squared(); distance_squared <= radius_squared } } @@ -265,7 +262,7 @@ mod aabb2d_tests { use super::Aabb2d; use crate::{ bounding::{BoundingCircle, BoundingVolume, IntersectsVolume}, - Vec2, + ops, Vec2, }; #[test] @@ -389,7 +386,7 @@ mod aabb2d_tests { max: Vec2::new(2.0, 2.0), }; let transformed = a.transformed_by(Vec2::new(2.0, -2.0), std::f32::consts::FRAC_PI_4); - let half_length = 2_f32.hypot(2.0); + let half_length = ops::hypot(2.0, 2.0); assert_eq!( transformed.min, Vec2::new(2.0 - half_length, -half_length - 2.0) @@ -472,16 +469,11 @@ impl BoundingCircle { } /// Computes a [`BoundingCircle`] containing the given set of points, - /// transformed by `translation` and `rotation`. + /// transformed by the rotation and translation of the given isometry. /// /// The bounding circle is not guaranteed to be the smallest possible. #[inline(always)] - pub fn from_point_cloud( - translation: Vec2, - rotation: impl Into, - points: &[Vec2], - ) -> BoundingCircle { - let rotation: Rot2 = rotation.into(); + pub fn from_point_cloud(isometry: Isometry2d, points: &[Vec2]) -> BoundingCircle { let center = point_cloud_2d_center(points); let mut radius_squared = 0.0; @@ -493,7 +485,7 @@ impl BoundingCircle { } } - BoundingCircle::new(rotation * center + translation, radius_squared.sqrt()) + BoundingCircle::new(isometry * center, radius_squared.sqrt()) } /// Get the radius of the bounding circle @@ -544,7 +536,7 @@ impl BoundingVolume for BoundingCircle { #[inline(always)] fn contains(&self, other: &Self) -> bool { let diff = self.radius() - other.radius(); - self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff) + self.center.distance_squared(other.center) <= diff.squared().copysign(diff) } #[inline(always)] @@ -602,7 +594,7 @@ impl IntersectsVolume for BoundingCircle { #[inline(always)] fn intersects(&self, other: &Self) -> bool { let center_distance_squared = self.center.distance_squared(other.center); - let radius_sum_squared = (self.radius() + other.radius()).powi(2); + let radius_sum_squared = (self.radius() + other.radius()).squared(); center_distance_squared <= radius_sum_squared } } diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 8afb3bf8c46253..f3cccbfb0fd1a4 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -1,12 +1,13 @@ //! Contains [`Bounded2d`] implementations for [geometric primitives](crate::primitives). use crate::{ + ops, primitives::{ - Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, CircularSegment, - Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, - Segment2d, Triangle2d, + Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, + CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, + Rhombus, Segment2d, Triangle2d, }, - Dir2, Mat2, Rot2, Vec2, + Dir2, Isometry2d, Mat2, Rot2, Vec2, }; use std::f32::consts::{FRAC_PI_2, PI, TAU}; @@ -15,12 +16,12 @@ use smallvec::SmallVec; use super::{Aabb2d, Bounded2d, BoundingCircle}; impl Bounded2d for Circle { - fn aabb_2d(&self, translation: Vec2, _rotation: impl Into) -> Aabb2d { - Aabb2d::new(translation, Vec2::splat(self.radius)) + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + Aabb2d::new(isometry.translation, Vec2::splat(self.radius)) } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, self.radius) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.radius) } } @@ -57,49 +58,52 @@ fn arc_bounding_points(arc: Arc2d, rotation: impl Into) -> SmallVec<[Vec2; } impl Bounded2d for Arc2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { // If our arc covers more than a circle, just return the bounding box of the circle. if self.half_angle >= PI { - return Circle::new(self.radius).aabb_2d(translation, rotation); + return Circle::new(self.radius).aabb_2d(isometry); } - Aabb2d::from_point_cloud(translation, 0.0, &arc_bounding_points(*self, rotation)) + Aabb2d::from_point_cloud( + Isometry2d::from_translation(isometry.translation), + &arc_bounding_points(*self, isometry.rotation), + ) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { // There are two possibilities for the bounding circle. if self.is_major() { // If the arc is major, then the widest distance between two points is a diameter of the arc's circle; // therefore, that circle is the bounding radius. - BoundingCircle::new(translation, self.radius) + BoundingCircle::new(isometry.translation, self.radius) } else { // Otherwise, the widest distance between two points is the chord, // so a circle of that diameter around the midpoint will contain the entire arc. - let center = rotation.into() * self.chord_midpoint(); - BoundingCircle::new(center + translation, self.half_chord_length()) + let center = isometry.rotation * self.chord_midpoint(); + BoundingCircle::new(center + isometry.translation, self.half_chord_length()) } } } impl Bounded2d for CircularSector { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { // If our sector covers more than a circle, just return the bounding box of the circle. if self.half_angle() >= PI { - return Circle::new(self.radius()).aabb_2d(translation, rotation); + return Circle::new(self.radius()).aabb_2d(isometry); } // Otherwise, we use the same logic as for Arc2d, above, just with the circle's center as an additional possibility. - let mut bounds = arc_bounding_points(self.arc, rotation); + let mut bounds = arc_bounding_points(self.arc, isometry.rotation); bounds.push(Vec2::ZERO); - Aabb2d::from_point_cloud(translation, 0.0, &bounds) + Aabb2d::from_point_cloud(Isometry2d::from_translation(isometry.translation), &bounds) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { if self.arc.is_major() { // If the arc is major, that is, greater than a semicircle, // then bounding circle is just the circle defining the sector. - BoundingCircle::new(translation, self.arc.radius) + BoundingCircle::new(isometry.translation, self.arc.radius) } else { // However, when the arc is minor, // we need our bounding circle to include both endpoints of the arc as well as the circle center. @@ -111,25 +115,23 @@ impl Bounded2d for CircularSector { self.arc.left_endpoint(), self.arc.right_endpoint(), ) - .bounding_circle(translation, rotation) + .bounding_circle(isometry) } } } impl Bounded2d for CircularSegment { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - self.arc.aabb_2d(translation, rotation) + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + self.arc.aabb_2d(isometry) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { - self.arc.bounding_circle(translation, rotation) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + self.arc.bounding_circle(isometry) } } impl Bounded2d for Ellipse { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { // V = (hh * cos(beta), hh * sin(beta)) // #####*##### // ### | ### @@ -142,7 +144,7 @@ impl Bounded2d for Ellipse { 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(); + let (alpha_sin, alpha_cos) = isometry.rotation.sin_cos(); // Sine and cosine of alpha + pi/2. We can avoid the trigonometric functions: // sin(beta) = sin(alpha + pi/2) = cos(alpha) @@ -153,43 +155,50 @@ impl Bounded2d for 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)); + let half_size = Vec2::new(ops::hypot(ux, vx), ops::hypot(uy, vy)); - Aabb2d::new(translation, half_size) + Aabb2d::new(isometry.translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, self.semi_major()) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.semi_major()) } } -impl Bounded2d for Rhombus { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation_mat = rotation.into(); +impl Bounded2d for Annulus { + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + Aabb2d::new(isometry.translation, Vec2::splat(self.outer_circle.radius)) + } + + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.outer_circle.radius) + } +} +impl Bounded2d for Rhombus { + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { let [rotated_x_half_diagonal, rotated_y_half_diagonal] = [ - rotation_mat * Vec2::new(self.half_diagonals.x, 0.0), - rotation_mat * Vec2::new(0.0, self.half_diagonals.y), + isometry.rotation * Vec2::new(self.half_diagonals.x, 0.0), + isometry.rotation * Vec2::new(0.0, self.half_diagonals.y), ]; let aabb_half_extent = rotated_x_half_diagonal .abs() .max(rotated_y_half_diagonal.abs()); Aabb2d { - min: -aabb_half_extent + translation, - max: aabb_half_extent + translation, + min: -aabb_half_extent + isometry.translation, + max: aabb_half_extent + isometry.translation, } } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, self.circumradius()) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.circumradius()) } } impl Bounded2d for Plane2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - let normal = rotation * *self.normal; + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + let normal = isometry.rotation * *self.normal; let facing_x = normal == Vec2::X || normal == Vec2::NEG_X; let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y; @@ -199,18 +208,17 @@ impl Bounded2d for Plane2d { 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) + Aabb2d::new(isometry.translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, f32::MAX / 2.0) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, f32::MAX / 2.0) } } impl Bounded2d for Line2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - let direction = rotation * *self.direction; + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + let direction = isometry.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. @@ -219,65 +227,62 @@ impl Bounded2d for Line2d { 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) + Aabb2d::new(isometry.translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, f32::MAX / 2.0) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, f32::MAX / 2.0) } } impl Bounded2d for Segment2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { // Rotate the segment by `rotation` - let rotation: Rot2 = rotation.into(); - let direction = rotation * *self.direction; + let direction = isometry.rotation * *self.direction; let half_size = (self.half_length * direction).abs(); - Aabb2d::new(translation, half_size) + Aabb2d::new(isometry.translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, self.half_length) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.half_length) } } impl Bounded2d for Polyline2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, &self.vertices) } } impl Bounded2d for BoxedPolyline2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, &self.vertices) } } impl Bounded2d for Triangle2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - let [a, b, c] = self.vertices.map(|vtx| rotation * vtx); + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + let [a, b, c] = self.vertices.map(|vtx| isometry.rotation * 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, + min: min + isometry.translation, + max: max + isometry.translation, } } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { - let rotation: Rot2 = rotation.into(); + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { let [a, b, c] = self.vertices; // The points of the segment opposite to the obtuse or right angle if one exists @@ -298,85 +303,79 @@ impl Bounded2d for Triangle2d { // 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 * center + translation, rotation) + segment.bounding_circle(isometry * Isometry2d::from_translation(center)) } else { // The triangle is acute, so the smallest bounding circle is the circumcircle. let (Circle { radius }, circumcenter) = self.circumcircle(); - BoundingCircle::new(rotation * circumcenter + translation, radius) + BoundingCircle::new(isometry * circumcenter, radius) } } } impl Bounded2d for Rectangle { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - + fn aabb_2d(&self, isometry: Isometry2d) -> 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 (sin, cos) = isometry.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) + Aabb2d::new(isometry.translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { let radius = self.half_size.length(); - BoundingCircle::new(translation, radius) + BoundingCircle::new(isometry.translation, radius) } } impl Bounded2d for Polygon { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, &self.vertices) } } impl Bounded2d for BoxedPolygon { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(translation, rotation, &self.vertices) + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { + Aabb2d::from_point_cloud(isometry, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::from_point_cloud(isometry, &self.vertices) } } impl Bounded2d for RegularPolygon { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { let mut min = Vec2::ZERO; let mut max = Vec2::ZERO; - for vertex in self.vertices(rotation.as_radians()) { + for vertex in self.vertices(isometry.rotation.as_radians()) { min = min.min(vertex); max = max.max(vertex); } Aabb2d { - min: min + translation, - max: max + translation, + min: min + isometry.translation, + max: max + isometry.translation, } } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, self.circumcircle.radius) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.circumcircle.radius) } } impl Bounded2d for Capsule2d { - fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { - let rotation: Rot2 = rotation.into(); - + fn aabb_2d(&self, isometry: Isometry2d) -> Aabb2d { // Get the line segment between the hemicircles of the rotated capsule let segment = Segment2d { // Multiplying a normalized vector (Vec2::Y) with a rotation returns a normalized vector. - direction: rotation * Dir2::Y, + direction: isometry.rotation * Dir2::Y, half_length: self.half_length, }; let (a, b) = (segment.point1(), segment.point2()); @@ -386,13 +385,13 @@ impl Bounded2d for Capsule2d { let max = a.max(b) + Vec2::splat(self.radius); Aabb2d { - min: min + translation, - max: max + translation, + min: min + isometry.translation, + max: max + isometry.translation, } } - fn bounding_circle(&self, translation: Vec2, _rotation: impl Into) -> BoundingCircle { - BoundingCircle::new(translation, self.radius + self.half_length) + fn bounding_circle(&self, isometry: Isometry2d) -> BoundingCircle { + BoundingCircle::new(isometry.translation, self.radius + self.half_length) } } @@ -405,23 +404,26 @@ mod tests { use crate::{ bounding::Bounded2d, + ops::{self, FloatPow}, primitives::{ - Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, Plane2d, - Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, + Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, + Triangle2d, }, - Dir2, + Dir2, Isometry2d, Rot2, }; #[test] fn circle() { let circle = Circle { radius: 1.0 }; let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = circle.aabb_2d(translation, 0.0); + let aabb = circle.aabb_2d(isometry); 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); + let bounding_circle = circle.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } @@ -440,6 +442,12 @@ mod tests { bounding_circle_radius: f32, } + impl TestCase { + fn isometry(&self) -> Isometry2d { + Isometry2d::new(self.translation, self.rotation.into()) + } + } + // The apothem of an arc covering 1/6th of a circle. let apothem = f32::sqrt(3.0) / 2.0; let tests = [ @@ -499,7 +507,7 @@ mod tests { // The exact coordinates here are not obvious, but can be computed by constructing // an altitude from the midpoint of the chord to the y-axis and using the right triangle // similarity theorem. - bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.powi(2)), + bounding_circle_center: Vec2::new(-apothem / 2.0, apothem.squared()), bounding_circle_radius: 0.5, }, // Test case: handling of axis-aligned extrema @@ -554,17 +562,17 @@ mod tests { println!("subtest case: {}", test.name); let segment: CircularSegment = test.arc.into(); - let arc_aabb = test.arc.aabb_2d(test.translation, test.rotation); + let arc_aabb = test.arc.aabb_2d(test.isometry()); assert_abs_diff_eq!(test.aabb_min, arc_aabb.min); assert_abs_diff_eq!(test.aabb_max, arc_aabb.max); - let segment_aabb = segment.aabb_2d(test.translation, test.rotation); + let segment_aabb = segment.aabb_2d(test.isometry()); assert_abs_diff_eq!(test.aabb_min, segment_aabb.min); assert_abs_diff_eq!(test.aabb_max, segment_aabb.max); - let arc_bounding_circle = test.arc.bounding_circle(test.translation, test.rotation); + let arc_bounding_circle = test.arc.bounding_circle(test.isometry()); assert_abs_diff_eq!(test.bounding_circle_center, arc_bounding_circle.center); assert_abs_diff_eq!(test.bounding_circle_radius, arc_bounding_circle.radius()); - let segment_bounding_circle = segment.bounding_circle(test.translation, test.rotation); + let segment_bounding_circle = segment.bounding_circle(test.isometry()); assert_abs_diff_eq!(test.bounding_circle_center, segment_bounding_circle.center); assert_abs_diff_eq!( test.bounding_circle_radius, @@ -586,6 +594,12 @@ mod tests { bounding_circle_radius: f32, } + impl TestCase { + fn isometry(&self) -> Isometry2d { + Isometry2d::new(self.translation, self.rotation.into()) + } + } + // The apothem of an arc covering 1/6th of a circle. let apothem = f32::sqrt(3.0) / 2.0; let inv_sqrt_3 = f32::sqrt(3.0).recip(); @@ -704,11 +718,11 @@ mod tests { println!("subtest case: {}", test.name); let sector: CircularSector = test.arc.into(); - let aabb = sector.aabb_2d(test.translation, test.rotation); + let aabb = sector.aabb_2d(test.isometry()); assert_abs_diff_eq!(test.aabb_min, aabb.min); assert_abs_diff_eq!(test.aabb_max, aabb.max); - let bounding_circle = sector.bounding_circle(test.translation, test.rotation); + let bounding_circle = sector.bounding_circle(test.isometry()); assert_abs_diff_eq!(test.bounding_circle_center, bounding_circle.center); assert_abs_diff_eq!(test.bounding_circle_radius, bounding_circle.radius()); } @@ -718,37 +732,57 @@ mod tests { fn ellipse() { let ellipse = Ellipse::new(1.0, 0.5); let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = ellipse.aabb_2d(translation, 0.0); + let aabb = ellipse.aabb_2d(isometry); 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); + let bounding_circle = ellipse.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } + #[test] + fn annulus() { + let annulus = Annulus::new(1.0, 2.0); + let translation = Vec2::new(2.0, 1.0); + let rotation = Rot2::radians(1.0); + let isometry = Isometry2d::new(translation, rotation); + + let aabb = annulus.aabb_2d(isometry); + assert_eq!(aabb.min, Vec2::new(0.0, -1.0)); + assert_eq!(aabb.max, Vec2::new(4.0, 3.0)); + + let bounding_circle = annulus.bounding_circle(isometry); + assert_eq!(bounding_circle.center, translation); + assert_eq!(bounding_circle.radius(), 2.0); + } + #[test] fn rhombus() { let rhombus = Rhombus::new(2.0, 1.0); let translation = Vec2::new(2.0, 1.0); + let rotation = Rot2::radians(FRAC_PI_4); + let isometry = Isometry2d::new(translation, rotation); - let aabb = rhombus.aabb_2d(translation, std::f32::consts::FRAC_PI_4); + let aabb = rhombus.aabb_2d(isometry); assert_eq!(aabb.min, Vec2::new(1.2928932, 0.29289323)); assert_eq!(aabb.max, Vec2::new(2.7071068, 1.7071068)); - let bounding_circle = rhombus.bounding_circle(translation, std::f32::consts::FRAC_PI_4); + let bounding_circle = rhombus.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); let rhombus = Rhombus::new(0.0, 0.0); let translation = Vec2::new(0.0, 0.0); + let isometry = Isometry2d::new(translation, rotation); - let aabb = rhombus.aabb_2d(translation, std::f32::consts::FRAC_PI_4); + let aabb = rhombus.aabb_2d(isometry); assert_eq!(aabb.min, Vec2::new(0.0, 0.0)); assert_eq!(aabb.max, Vec2::new(0.0, 0.0)); - let bounding_circle = rhombus.bounding_circle(translation, std::f32::consts::FRAC_PI_4); + let bounding_circle = rhombus.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 0.0); } @@ -756,20 +790,21 @@ mod tests { #[test] fn plane() { let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb1 = Plane2d::new(Vec2::X).aabb_2d(translation, 0.0); + let aabb1 = Plane2d::new(Vec2::X).aabb_2d(isometry); 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); + let aabb2 = Plane2d::new(Vec2::Y).aabb_2d(isometry); 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); + let aabb3 = Plane2d::new(Vec2::ONE).aabb_2d(isometry); 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); + let bounding_circle = Plane2d::new(Vec2::Y).bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); } @@ -777,23 +812,24 @@ mod tests { #[test] fn line() { let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(translation, 0.0); + let aabb1 = Line2d { direction: Dir2::Y }.aabb_2d(isometry); 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: Dir2::X }.aabb_2d(translation, 0.0); + let aabb2 = Line2d { direction: Dir2::X }.aabb_2d(isometry); 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: Dir2::from_xy(1.0, 1.0).unwrap(), } - .aabb_2d(translation, 0.0); + .aabb_2d(isometry); 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: Dir2::Y }.bounding_circle(translation, 0.0); + let bounding_circle = Line2d { direction: Dir2::Y }.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), f32::MAX / 2.0); } @@ -801,15 +837,16 @@ mod tests { #[test] fn segment() { let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); 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); + let aabb = segment.aabb_2d(isometry); 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); + let bounding_circle = segment.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5)); + assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5)); } #[test] @@ -821,12 +858,13 @@ mod tests { Vec2::new(1.0, -1.0), ]); let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = polyline.aabb_2d(translation, 0.0); + let aabb = polyline.aabb_2d(isometry); 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); + let bounding_circle = polyline.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2); } @@ -836,14 +874,15 @@ mod tests { 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 isometry = Isometry2d::from_translation(translation); - let aabb = acute_triangle.aabb_2d(translation, 0.0); + let aabb = acute_triangle.aabb_2d(isometry); 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); + let bounding_circle = acute_triangle.bounding_circle(isometry); assert_eq!(bounding_circle.center, circumcenter + translation); assert_eq!(bounding_circle.radius(), radius); } @@ -856,13 +895,14 @@ mod tests { Vec2::new(10.0, -1.0), ); let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = obtuse_triangle.aabb_2d(translation, 0.0); + let aabb = obtuse_triangle.aabb_2d(isometry); 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); + let bounding_circle = obtuse_triangle.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation - Vec2::Y); assert_eq!(bounding_circle.radius(), 10.0); } @@ -872,14 +912,14 @@ mod tests { 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 aabb = rectangle.aabb_2d(Isometry2d::new(translation, Rot2::radians(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); + let bounding_circle = rectangle.bounding_circle(Isometry2d::from_translation(translation)); assert_eq!(bounding_circle.center, translation); - assert_eq!(bounding_circle.radius(), 1.0_f32.hypot(0.5)); + assert_eq!(bounding_circle.radius(), ops::hypot(1.0, 0.5)); } #[test] @@ -891,12 +931,13 @@ mod tests { Vec2::new(1.0, -1.0), ]); let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = polygon.aabb_2d(translation, 0.0); + let aabb = polygon.aabb_2d(isometry); 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); + let bounding_circle = polygon.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), std::f32::consts::SQRT_2); } @@ -905,12 +946,13 @@ mod tests { fn regular_polygon() { let regular_polygon = RegularPolygon::new(1.0, 5); let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = regular_polygon.aabb_2d(translation, 0.0); + let aabb = regular_polygon.aabb_2d(isometry); 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); + let bounding_circle = regular_polygon.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.0); } @@ -919,12 +961,13 @@ mod tests { fn capsule() { let capsule = Capsule2d::new(0.5, 2.0); let translation = Vec2::new(2.0, 1.0); + let isometry = Isometry2d::from_translation(translation); - let aabb = capsule.aabb_2d(translation, 0.0); + let aabb = capsule.aabb_2d(isometry); assert_eq!(aabb.min, translation - Vec2::new(0.5, 1.5)); assert_eq!(aabb.max, translation + Vec2::new(0.5, 1.5)); - let bounding_circle = capsule.bounding_circle(translation, 0.0); + let bounding_circle = capsule.bounding_circle(isometry); assert_eq!(bounding_circle.center, translation); assert_eq!(bounding_circle.radius(), 1.5); } diff --git a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs index f6aac82d97c239..631d33790cca89 100644 --- a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs +++ b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs @@ -7,194 +7,176 @@ use crate::primitives::{ BoxedPolygon, BoxedPolyline2d, Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }; -use crate::{Quat, Vec3}; +use crate::{ops, Isometry2d, Isometry3d, Quat, Rot2}; use crate::{bounding::Bounded2d, primitives::Circle}; use super::{Aabb3d, Bounded3d, BoundingSphere}; impl BoundedExtrusion for Circle { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { // Reference: http://iquilezles.org/articles/diskbbox/ - let segment_dir = rotation * Vec3::Z; + let segment_dir = isometry.rotation * Vec3A::Z; let top = (segment_dir * half_depth).abs(); - let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO); - let half_size = self.radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); Aabb3d { - min: (translation - half_size - top).into(), - max: (translation + half_size + top).into(), + min: isometry.translation - half_size - top, + max: isometry.translation + half_size + top, } } } impl BoundedExtrusion for Ellipse { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { let Vec2 { x: a, y: b } = self.half_size; - let normal = rotation * Vec3::Z; - let conjugate_rot = rotation.conjugate(); + let normal = isometry.rotation * Vec3A::Z; + let conjugate_rot = isometry.rotation.conjugate(); - let [max_x, max_y, max_z] = Vec3::AXES.map(|axis: Vec3| { + let [max_x, max_y, max_z] = Vec3A::AXES.map(|axis| { let Some(axis) = (conjugate_rot * axis.reject_from(normal)) .xy() .try_normalize() else { - return Vec3::ZERO; + return Vec3A::ZERO; }; if axis.element_product() == 0. { - return rotation * Vec3::new(a * axis.y, b * axis.x, 0.); + return isometry.rotation * Vec3A::new(a * axis.y, b * axis.x, 0.); } let m = -axis.x / axis.y; let signum = axis.signum(); let y = signum.y * b * b / (b * b + m * m * a * a).sqrt(); let x = signum.x * a * (1. - y * y / b / b).sqrt(); - rotation * Vec3::new(x, y, 0.) + isometry.rotation * Vec3A::new(x, y, 0.) }); - let half_size = Vec3::new(max_x.x, max_y.y, max_z.z).abs() + (normal * half_depth).abs(); - Aabb3d::new(translation, half_size) + let half_size = Vec3A::new(max_x.x, max_y.y, max_z.z).abs() + (normal * half_depth).abs(); + Aabb3d::new(isometry.translation, half_size) } } impl BoundedExtrusion for Line2d { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let dir = rotation * self.direction.extend(0.); - let half_depth = (rotation * Vec3::new(0., 0., half_depth)).abs(); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let dir = isometry.rotation * Vec3A::from(self.direction.extend(0.)); + let half_depth = (isometry.rotation * Vec3A::new(0., 0., half_depth)).abs(); let max = f32::MAX / 2.; - let half_size = Vec3::new( + let half_size = Vec3A::new( if dir.x == 0. { half_depth.x } else { max }, if dir.y == 0. { half_depth.y } else { max }, if dir.z == 0. { half_depth.z } else { max }, ); - Aabb3d::new(translation, half_size) + Aabb3d::new(isometry.translation, half_size) } } impl BoundedExtrusion for Segment2d { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let half_size = rotation * self.point1().extend(0.); - let depth = rotation * Vec3::new(0., 0., half_depth); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let half_size = isometry.rotation * Vec3A::from(self.point1().extend(0.)); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - Aabb3d::new(translation, half_size.abs() + depth.abs()) + Aabb3d::new(isometry.translation, half_size.abs() + depth.abs()) } } impl BoundedExtrusion for Polyline2d { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let aabb = Aabb3d::from_point_cloud( - translation, - rotation, - self.vertices.map(|v| v.extend(0.)).into_iter(), - ); - let depth = rotation * Vec3A::new(0., 0., half_depth); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let aabb = + Aabb3d::from_point_cloud(isometry, self.vertices.map(|v| v.extend(0.)).into_iter()); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); aabb.grow(depth.abs()) } } impl BoundedExtrusion for BoxedPolyline2d { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let aabb = Aabb3d::from_point_cloud( - translation, - rotation, - self.vertices.iter().map(|v| v.extend(0.)), - ); - let depth = rotation * Vec3A::new(0., 0., half_depth); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); aabb.grow(depth.abs()) } } impl BoundedExtrusion for Triangle2d { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let aabb = Aabb3d::from_point_cloud( - translation, - rotation, - self.vertices.iter().map(|v| v.extend(0.)), - ); - let depth = rotation * Vec3A::new(0., 0., half_depth); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); aabb.grow(depth.abs()) } } impl BoundedExtrusion for Rectangle { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { Cuboid { half_size: self.half_size.extend(half_depth), } - .aabb_3d(translation, rotation) + .aabb_3d(isometry) } } impl BoundedExtrusion for Polygon { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let aabb = Aabb3d::from_point_cloud( - translation, - rotation, - self.vertices.map(|v| v.extend(0.)).into_iter(), - ); - let depth = rotation * Vec3A::new(0., 0., half_depth); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let aabb = + Aabb3d::from_point_cloud(isometry, self.vertices.map(|v| v.extend(0.)).into_iter()); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); aabb.grow(depth.abs()) } } impl BoundedExtrusion for BoxedPolygon { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let aabb = Aabb3d::from_point_cloud( - translation, - rotation, - self.vertices.iter().map(|v| v.extend(0.)), - ); - let depth = rotation * Vec3A::new(0., 0., half_depth); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); aabb.grow(depth.abs()) } } impl BoundedExtrusion for RegularPolygon { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { let aabb = Aabb3d::from_point_cloud( - translation, - rotation, + isometry, self.vertices(0.).into_iter().map(|v| v.extend(0.)), ); - let depth = rotation * Vec3A::new(0., 0., half_depth); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); aabb.grow(depth.abs()) } } impl BoundedExtrusion for Capsule2d { - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { let aabb = Cylinder { half_height: half_depth, radius: self.radius, } - .aabb_3d(Vec3::ZERO, rotation * Quat::from_rotation_x(FRAC_PI_2)); + .aabb_3d(Isometry3d::from_rotation( + isometry.rotation * Quat::from_rotation_x(FRAC_PI_2), + )); - let up = rotation * Vec3::new(0., self.half_length, 0.); - let half_size = Into::::into(aabb.max) + up.abs(); - Aabb3d::new(translation, half_size) + let up = isometry.rotation * Vec3A::new(0., self.half_length, 0.); + let half_size = aabb.max + up.abs(); + Aabb3d::new(isometry.translation, half_size) } } impl Bounded3d for Extrusion { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { - self.base_shape - .extrusion_aabb_3d(self.half_depth, translation, rotation) + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { + self.base_shape.extrusion_aabb_3d(self.half_depth, isometry) } - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { self.base_shape - .extrusion_bounding_sphere(self.half_depth, translation, rotation) + .extrusion_bounding_sphere(self.half_depth, isometry) } } @@ -206,12 +188,12 @@ impl Bounded3d for Extrusion { /// `impl BoundedExtrusion for MyShape {}` pub trait BoundedExtrusion: Primitive2d + Bounded2d { /// Get an axis-aligned bounding box for an extrusion with this shape as a base and the given `half_depth`, transformed by the given `translation` and `rotation`. - fn extrusion_aabb_3d(&self, half_depth: f32, translation: Vec3, rotation: Quat) -> Aabb3d { - let cap_normal = rotation * Vec3::Z; - let conjugate_rot = rotation.conjugate(); + fn extrusion_aabb_3d(&self, half_depth: f32, isometry: Isometry3d) -> Aabb3d { + let cap_normal = isometry.rotation * Vec3A::Z; + let conjugate_rot = isometry.rotation.conjugate(); // The `(halfsize, offset)` for each axis - let axis_values = Vec3::AXES.map(|ax| { + let axis_values = Vec3A::AXES.map(|ax| { // This is the direction of the line of intersection of a plane with the `ax` normal and the plane containing the cap of the extrusion. let intersect_line = ax.cross(cap_normal); if intersect_line.length_squared() <= f32::EPSILON { @@ -228,24 +210,19 @@ pub trait BoundedExtrusion: Primitive2d + Bounded2d { // Calculate the `Aabb2d` of the base shape. The shape is rotated so that the line of intersection is parallel to the Y axis in the `Aabb2d` calculations. // This guarantees that the X value of the `Aabb2d` is closest to the `ax` plane - let aabb2d = self.aabb_2d(Vec2::ZERO, angle); + let aabb2d = self.aabb_2d(Isometry2d::from_rotation(Rot2::radians(angle))); (aabb2d.half_size().x * scale, aabb2d.center().x * scale) }); let offset = Vec3A::from_array(axis_values.map(|(_, offset)| offset)); let cap_size = Vec3A::from_array(axis_values.map(|(max_val, _)| max_val)).abs(); - let depth = rotation * Vec3A::new(0., 0., half_depth); + let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - Aabb3d::new(Vec3A::from(translation) - offset, cap_size + depth.abs()) + Aabb3d::new(isometry.translation - offset, cap_size + depth.abs()) } /// Get a bounding sphere for an extrusion of the `base_shape` with the given `half_depth` with the given translation and rotation - fn extrusion_bounding_sphere( - &self, - half_depth: f32, - translation: Vec3, - rotation: Quat, - ) -> BoundingSphere { + fn extrusion_bounding_sphere(&self, half_depth: f32, isometry: Isometry3d) -> BoundingSphere { // We calculate the bounding circle of the base shape. // Since each of the extrusions bases will have the same distance from its center, // and they are just shifted along the Z-axis, the minimum bounding sphere will be the bounding sphere @@ -253,9 +230,9 @@ pub trait BoundedExtrusion: Primitive2d + Bounded2d { let BoundingCircle { center, circle: Circle { radius }, - } = self.bounding_circle(Vec2::ZERO, 0.); - let radius = radius.hypot(half_depth); - let center = translation + rotation * center.extend(0.); + } = self.bounding_circle(Isometry2d::IDENTITY); + let radius = ops::hypot(radius, half_depth); + let center = isometry * Vec3A::from(center.extend(0.)); BoundingSphere::new(center, radius) } @@ -269,25 +246,27 @@ mod tests { use crate::{ bounding::{Bounded3d, BoundingVolume}, + ops, primitives::{ Capsule2d, Circle, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }, - Dir2, + Dir2, Isometry3d, }; #[test] fn circle() { let cylinder = Extrusion::new(Circle::new(0.5), 2.0); let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = cylinder.aabb_3d(translation, Quat::IDENTITY); + let aabb = cylinder.aabb_3d(isometry); assert_eq!(aabb.center(), Vec3A::from(translation)); assert_eq!(aabb.half_size(), Vec3A::new(0.5, 0.5, 1.0)); - let bounding_sphere = cylinder.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = cylinder.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1f32.hypot(0.5)); + assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); } #[test] @@ -295,12 +274,13 @@ mod tests { let extrusion = Extrusion::new(Ellipse::new(2.0, 0.5), 4.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_euler(EulerRot::ZYX, FRAC_PI_4, FRAC_PI_4, FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), Vec3A::from(translation)); assert_eq!(aabb.half_size(), Vec3A::new(2.709784, 1.3801551, 2.436141)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 8f32.sqrt()); } @@ -315,12 +295,13 @@ mod tests { ); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_y(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(1.5857864, f32::MIN / 2., 3.5857865)); assert_eq!(aabb.max, Vec3A::new(4.4142136, f32::MAX / 2., 6.414213)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center(), translation.into()); assert_eq!(bounding_sphere.radius(), f32::MAX / 2.); } @@ -329,13 +310,14 @@ mod tests { fn rectangle() { let extrusion = Extrusion::new(Rectangle::new(2.0, 1.0), 4.0); let translation = Vec3::new(3., 4., 5.); - let rotation = Quat::from_rotation_z(std::f32::consts::FRAC_PI_4); + let rotation = Quat::from_rotation_z(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), translation.into()); assert_eq!(aabb.half_size(), Vec3A::new(1.0606602, 1.0606602, 2.)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 2.291288); } @@ -345,12 +327,13 @@ mod tests { let extrusion = Extrusion::new(Segment2d::new(Dir2::new_unchecked(Vec2::NEG_Y), 3.), 4.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), translation.into()); assert_eq!(aabb.half_size(), Vec3A::new(0., 2.4748735, 2.4748735)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 2.5); } @@ -366,12 +349,13 @@ mod tests { let extrusion = Extrusion::new(polyline, 3.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), translation.into()); assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 2.0615528); } @@ -386,12 +370,13 @@ mod tests { let extrusion = Extrusion::new(triangle, 3.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), translation.into()); assert_eq!(aabb.half_size(), Vec3A::new(10., 1.7677668, 1.7677668)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!( bounding_sphere.center, Vec3A::new(3.0, 3.2928934, 4.2928934) @@ -410,12 +395,13 @@ mod tests { let extrusion = Extrusion::new(polygon, 3.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), translation.into()); assert_eq!(aabb.half_size(), Vec3A::new(1., 1.7677668, 1.7677668)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 2.0615528); } @@ -425,8 +411,9 @@ mod tests { let extrusion = Extrusion::new(RegularPolygon::new(2.0, 7), 4.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!( aabb.center(), Vec3A::from(translation) + Vec3A::new(0., 0.0700254, 0.0700254) @@ -436,7 +423,7 @@ mod tests { Vec3A::new(1.9498558, 2.7584014, 2.7584019) ); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 8f32.sqrt()); } @@ -446,12 +433,13 @@ mod tests { let extrusion = Extrusion::new(Capsule2d::new(0.5, 2.0), 4.0); let translation = Vec3::new(3., 4., 5.); let rotation = Quat::from_rotation_x(FRAC_PI_4); + let isometry = Isometry3d::new(translation, rotation); - let aabb = extrusion.aabb_3d(translation, rotation); + let aabb = extrusion.aabb_3d(isometry); assert_eq!(aabb.center(), translation.into()); assert_eq!(aabb.half_size(), Vec3A::new(0.5, 2.4748735, 2.4748735)); - let bounding_sphere = extrusion.bounding_sphere(translation, rotation); + let bounding_sphere = extrusion.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 2.5); } diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index 4e67a4d4084f2d..60822fa3fe4cdc 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -4,7 +4,7 @@ mod primitive_impls; use glam::Mat3; use super::{BoundingVolume, IntersectsVolume}; -use crate::{Quat, Vec3, Vec3A}; +use crate::{ops::FloatPow, Isometry3d, Quat, Vec3A}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -24,12 +24,12 @@ fn point_cloud_3d_center(points: impl Iterator>) -> Vec3 acc / len as f32 } -/// A trait with methods that return 3D bounded volumes for a shape +/// A trait with methods that return 3D bounding 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; + /// Get an axis-aligned bounding box for the shape translated and rotated by the given isometry. + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d; + /// Get a bounding sphere for the shape translated and rotated by the given isometry. + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere; } /// A 3D axis-aligned bounding box @@ -55,19 +55,18 @@ impl Aabb3d { } /// Computes the smallest [`Aabb3d`] containing the given set of points, - /// transformed by `translation` and `rotation`. + /// transformed by the rotation and translation of the given isometry. /// /// # Panics /// /// Panics if the given set of points is empty. #[inline(always)] pub fn from_point_cloud( - translation: impl Into, - rotation: Quat, + isometry: Isometry3d, points: impl Iterator>, ) -> Aabb3d { // Transform all points by rotation - let mut iter = points.map(|point| rotation * point.into()); + let mut iter = points.map(|point| isometry.rotation * point.into()); let first = iter .next() @@ -77,10 +76,9 @@ impl Aabb3d { (point.min(prev_min), point.max(prev_max)) }); - let translation = translation.into(); Aabb3d { - min: min + translation, - max: max + translation, + min: min + isometry.translation, + max: max + isometry.translation, } } @@ -255,7 +253,7 @@ impl IntersectsVolume for Aabb3d { fn intersects(&self, sphere: &BoundingSphere) -> bool { let closest_point = self.closest_point(sphere.center); let distance_squared = sphere.center.distance_squared(closest_point); - let radius_squared = sphere.radius().powi(2); + let radius_squared = sphere.radius().squared(); distance_squared <= radius_squared } } @@ -265,7 +263,7 @@ mod aabb3d_tests { use super::Aabb3d; use crate::{ bounding::{BoundingSphere, BoundingVolume, IntersectsVolume}, - Quat, Vec3, Vec3A, + ops, Quat, Vec3, Vec3A, }; #[test] @@ -391,7 +389,7 @@ mod aabb3d_tests { Vec3A::new(2.0, -2.0, 4.0), Quat::from_rotation_z(std::f32::consts::FRAC_PI_4), ); - let half_length = 2_f32.hypot(2.0); + let half_length = ops::hypot(2.0, 2.0); assert_eq!( transformed.min, Vec3A::new(2.0 - half_length, -half_length - 2.0, 2.0) @@ -473,13 +471,12 @@ impl BoundingSphere { } /// Computes a [`BoundingSphere`] containing the given set of points, - /// transformed by `translation` and `rotation`. + /// transformed by the rotation and translation of the given isometry. /// /// The bounding sphere is not guaranteed to be the smallest possible. #[inline(always)] pub fn from_point_cloud( - translation: impl Into, - rotation: Quat, + isometry: Isometry3d, points: &[impl Copy + Into], ) -> BoundingSphere { let center = point_cloud_3d_center(points.iter().map(|v| Into::::into(*v))); @@ -493,10 +490,7 @@ impl BoundingSphere { } } - BoundingSphere::new( - rotation * center + translation.into(), - radius_squared.sqrt(), - ) + BoundingSphere::new(isometry * center, radius_squared.sqrt()) } /// Get the radius of the bounding sphere @@ -524,7 +518,7 @@ impl BoundingSphere { let radius = self.radius(); let distance_squared = (point - self.center).length_squared(); - if distance_squared <= radius.powi(2) { + if distance_squared <= radius.squared() { // The point is inside the sphere. point } else { @@ -559,7 +553,7 @@ impl BoundingVolume for BoundingSphere { #[inline(always)] fn contains(&self, other: &Self) -> bool { let diff = self.radius() - other.radius(); - self.center.distance_squared(other.center) <= diff.powi(2).copysign(diff) + self.center.distance_squared(other.center) <= diff.squared().copysign(diff) } #[inline(always)] @@ -627,7 +621,7 @@ impl IntersectsVolume for BoundingSphere { #[inline(always)] fn intersects(&self, other: &Self) -> bool { let center_distance_squared = self.center.distance_squared(other.center); - let radius_sum_squared = (self.radius() + other.radius()).powi(2); + let radius_sum_squared = (self.radius() + other.radius()).squared(); center_distance_squared <= radius_sum_squared } } diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index efd605a0d996e9..8827b0f253b618 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -1,29 +1,32 @@ //! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). +use glam::Vec3A; + use crate::{ bounding::{Bounded2d, BoundingCircle}, + ops, primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, }, - Dir3, Mat3, Quat, Vec2, Vec3, + Isometry2d, Isometry3d, Mat3, Vec2, Vec3, }; 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 aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { + Aabb3d::new(isometry.translation, Vec3::splat(self.radius)) } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, self.radius) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.translation, self.radius) } } impl Bounded3d for InfinitePlane3d { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { - let normal = rotation * *self.normal; + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { + let normal = isometry.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; @@ -33,19 +36,19 @@ impl Bounded3d for InfinitePlane3d { 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); + let half_size = Vec3A::new(half_width, half_height, half_depth); - Aabb3d::new(translation, half_size) + Aabb3d::new(isometry.translation, half_size) } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, f32::MAX / 2.0) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.translation, f32::MAX / 2.0) } } impl Bounded3d for Line3d { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { - let direction = rotation * *self.direction; + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { + let direction = isometry.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. @@ -53,55 +56,55 @@ impl Bounded3d for Line3d { 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); + let half_size = Vec3A::new(half_width, half_height, half_depth); - Aabb3d::new(translation, half_size) + Aabb3d::new(isometry.translation, half_size) } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, f32::MAX / 2.0) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.translation, f32::MAX / 2.0) } } impl Bounded3d for Segment3d { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { // Rotate the segment by `rotation` - let direction = rotation * *self.direction; + let direction = isometry.rotation * *self.direction; let half_size = (self.half_length * direction).abs(); - Aabb3d::new(translation, half_size) + Aabb3d::new(isometry.translation, half_size) } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, self.half_length) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.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.iter().copied()) + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { + Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) } - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { - BoundingSphere::from_point_cloud(translation, rotation, &self.vertices) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::from_point_cloud(isometry, &self.vertices) } } impl Bounded3d for BoxedPolyline3d { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { - Aabb3d::from_point_cloud(translation, rotation, self.vertices.iter().copied()) + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { + Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) } - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { - BoundingSphere::from_point_cloud(translation, rotation, &self.vertices) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::from_point_cloud(isometry, &self.vertices) } } impl Bounded3d for Cuboid { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> 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 rot_mat = Mat3::from_quat(isometry.rotation); let abs_rot_mat = Mat3::from_cols( rot_mat.x_axis.abs(), rot_mat.y_axis.abs(), @@ -109,80 +112,77 @@ impl Bounded3d for Cuboid { ); let half_size = abs_rot_mat * self.half_size; - Aabb3d::new(translation, half_size) + Aabb3d::new(isometry.translation, half_size) } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, self.half_size.length()) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.translation, self.half_size.length()) } } impl Bounded3d for Cylinder { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { // Reference: http://iquilezles.org/articles/diskbbox/ - let segment_dir = rotation * Vec3::Y; + let segment_dir = isometry.rotation * Vec3A::Y; let top = segment_dir * self.half_height; let bottom = -top; - let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO); - let half_size = self.radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_size = self.radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); Aabb3d { - min: (translation + (top - half_size).min(bottom - half_size)).into(), - max: (translation + (top + half_size).max(bottom + half_size)).into(), + min: isometry.translation + (top - half_size).min(bottom - half_size), + max: isometry.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) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + let radius = ops::hypot(self.radius, self.half_height); + BoundingSphere::new(isometry.translation, radius) } } impl Bounded3d for Capsule3d { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> 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: rotation * Dir3::Y, - half_length: self.half_length, - }; - let (a, b) = (segment.point1(), segment.point2()); + let segment_dir = isometry.rotation * Vec3A::Y; + let top = segment_dir * self.half_length; + let bottom = -top; // 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); + let min = bottom.min(top) - Vec3A::splat(self.radius); + let max = bottom.max(top) + Vec3A::splat(self.radius); Aabb3d { - min: (min + translation).into(), - max: (max + translation).into(), + min: min + isometry.translation, + max: max + isometry.translation, } } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, self.radius + self.half_length) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.translation, self.radius + self.half_length) } } impl Bounded3d for Cone { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { // Reference: http://iquilezles.org/articles/diskbbox/ - let segment_dir = rotation * Vec3::Y; + let segment_dir = isometry.rotation * Vec3A::Y; let top = segment_dir * 0.5 * self.height; let bottom = -top; - let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO); - let half_extents = Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_extents = Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); Aabb3d { - min: (translation + top.min(bottom - self.radius * half_extents)).into(), - max: (translation + top.max(bottom + self.radius * half_extents)).into(), + min: isometry.translation + top.min(bottom - self.radius * half_extents), + max: isometry.translation + top.max(bottom + self.radius * half_extents), } } - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { // Get the triangular cross-section of the cone. let half_height = 0.5 * self.height; let triangle = Triangle2d::new( @@ -193,36 +193,37 @@ impl Bounded3d for Cone { // 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); + let BoundingCircle { circle, center } = triangle.bounding_circle(Isometry2d::IDENTITY); - BoundingSphere::new(rotation * center.extend(0.0) + translation, circle.radius) + BoundingSphere::new( + isometry.rotation * Vec3A::from(center.extend(0.0)) + isometry.translation, + circle.radius, + ) } } impl Bounded3d for ConicalFrustum { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { // Reference: http://iquilezles.org/articles/diskbbox/ - let segment_dir = rotation * Vec3::Y; + let segment_dir = isometry.rotation * Vec3A::Y; let top = segment_dir * 0.5 * self.height; let bottom = -top; - let e = (Vec3::ONE - segment_dir * segment_dir).max(Vec3::ZERO); - let half_extents = Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let e = (Vec3A::ONE - segment_dir * segment_dir).max(Vec3A::ZERO); + let half_extents = Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); Aabb3d { - min: (translation + min: isometry.translation + (top - self.radius_top * half_extents) - .min(bottom - self.radius_bottom * half_extents)) - .into(), - max: (translation + .min(bottom - self.radius_bottom * half_extents), + max: isometry.translation + (top + self.radius_top * half_extents) - .max(bottom + self.radius_bottom * half_extents)) - .into(), + .max(bottom + self.radius_bottom * half_extents), } } - fn bounding_sphere(&self, translation: Vec3, rotation: Quat) -> BoundingSphere { + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { let half_height = 0.5 * self.height; // To compute the bounding sphere, we'll get the center and radius of the circumcircle @@ -277,42 +278,45 @@ impl Bounded3d for ConicalFrustum { (circumcenter, a.distance(circumcenter)) }; - BoundingSphere::new(translation + rotation * center.extend(0.0), radius) + BoundingSphere::new( + isometry.translation + isometry.rotation * Vec3A::from(center.extend(0.0)), + radius, + ) } } impl Bounded3d for Torus { - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> 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 = (Vec3::ONE - normal * normal).max(Vec3::ZERO); - let disc_half_size = self.major_radius * Vec3::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); + let normal = isometry.rotation * Vec3A::Y; + let e = (Vec3A::ONE - normal * normal).max(Vec3A::ZERO); + let disc_half_size = self.major_radius * Vec3A::new(e.x.sqrt(), e.y.sqrt(), e.z.sqrt()); // Expand the disc by the minor radius to get the torus half-size - let half_size = disc_half_size + Vec3::splat(self.minor_radius); + let half_size = disc_half_size + Vec3A::splat(self.minor_radius); - Aabb3d::new(translation, half_size) + Aabb3d::new(isometry.translation, half_size) } - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { - BoundingSphere::new(translation, self.outer_radius()) + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { + BoundingSphere::new(isometry.translation, self.outer_radius()) } } impl Bounded3d for Triangle3d { /// Get the bounding box of the triangle. - fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + fn aabb_3d(&self, isometry: Isometry3d) -> Aabb3d { let [a, b, c] = self.vertices; - let a = rotation * a; - let b = rotation * b; - let c = rotation * c; + let a = isometry.rotation * a; + let b = isometry.rotation * b; + let c = isometry.rotation * c; - let min = a.min(b).min(c); - let max = a.max(b).max(c); + let min = Vec3A::from(a.min(b).min(c)); + let max = Vec3A::from(a.max(b).max(c)); - let bounding_center = (max + min) / 2.0 + translation; + let bounding_center = (max + min) / 2.0 + isometry.translation; let half_extents = (max - min) / 2.0; Aabb3d::new(bounding_center, half_extents) @@ -323,25 +327,26 @@ impl Bounded3d for Triangle3d { /// The [`Triangle3d`] implements the minimal bounding sphere calculation. For acute triangles, the circumcenter is used as /// the center of the sphere. For the others, the bounding sphere is the minimal sphere /// that contains the largest side of the triangle. - fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + fn bounding_sphere(&self, isometry: Isometry3d) -> BoundingSphere { if self.is_degenerate() || self.is_obtuse() { let (p1, p2) = self.largest_side(); + let (p1, p2) = (Vec3A::from(p1), Vec3A::from(p2)); let mid_point = (p1 + p2) / 2.0; let radius = mid_point.distance(p1); - BoundingSphere::new(mid_point + translation, radius) + BoundingSphere::new(mid_point + isometry.translation, radius) } else { let [a, _, _] = self.vertices; let circumcenter = self.circumcenter(); let radius = circumcenter.distance(a); - BoundingSphere::new(circumcenter + translation, radius) + BoundingSphere::new(Vec3A::from(circumcenter) + isometry.translation, radius) } } } #[cfg(test)] mod tests { - use crate::bounding::BoundingVolume; + use crate::{bounding::BoundingVolume, ops, Isometry3d}; use glam::{Quat, Vec3, Vec3A}; use crate::{ @@ -357,12 +362,13 @@ mod tests { fn sphere() { let sphere = Sphere { radius: 1.0 }; let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = sphere.aabb_3d(translation, Quat::IDENTITY); + let aabb = sphere.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - let bounding_sphere = sphere.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = sphere.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 1.0); } @@ -370,25 +376,25 @@ mod tests { #[test] fn plane() { let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation, Quat::IDENTITY); + let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(isometry); assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0)); assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0)); - let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(translation, Quat::IDENTITY); + let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(isometry); assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0)); assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0)); - let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(translation, Quat::IDENTITY); + let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(isometry); assert_eq!(aabb3.min, Vec3A::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0)); assert_eq!(aabb3.max, Vec3A::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0)); - let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(translation, Quat::IDENTITY); + let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(isometry); assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0)); assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0)); - let bounding_sphere = - InfinitePlane3d::new(Vec3::Y).bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = InfinitePlane3d::new(Vec3::Y).bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); } @@ -396,28 +402,28 @@ mod tests { #[test] fn line() { let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb1 = Line3d { direction: Dir3::Y }.aabb_3d(translation, Quat::IDENTITY); + let aabb1 = Line3d { direction: Dir3::Y }.aabb_3d(isometry); assert_eq!(aabb1.min, Vec3A::new(2.0, -f32::MAX / 2.0, 0.0)); assert_eq!(aabb1.max, Vec3A::new(2.0, f32::MAX / 2.0, 0.0)); - let aabb2 = Line3d { direction: Dir3::X }.aabb_3d(translation, Quat::IDENTITY); + let aabb2 = Line3d { direction: Dir3::X }.aabb_3d(isometry); assert_eq!(aabb2.min, Vec3A::new(-f32::MAX / 2.0, 1.0, 0.0)); assert_eq!(aabb2.max, Vec3A::new(f32::MAX / 2.0, 1.0, 0.0)); - let aabb3 = Line3d { direction: Dir3::Z }.aabb_3d(translation, Quat::IDENTITY); + let aabb3 = Line3d { direction: Dir3::Z }.aabb_3d(isometry); assert_eq!(aabb3.min, Vec3A::new(2.0, 1.0, -f32::MAX / 2.0)); assert_eq!(aabb3.max, Vec3A::new(2.0, 1.0, f32::MAX / 2.0)); let aabb4 = Line3d { direction: Dir3::from_xyz(1.0, 1.0, 1.0).unwrap(), } - .aabb_3d(translation, Quat::IDENTITY); + .aabb_3d(isometry); assert_eq!(aabb4.min, Vec3A::splat(-f32::MAX / 2.0)); assert_eq!(aabb4.max, Vec3A::splat(f32::MAX / 2.0)); - let bounding_sphere = - Line3d { direction: Dir3::Y }.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = Line3d { direction: Dir3::Y }.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); } @@ -425,16 +431,18 @@ mod tests { #[test] fn segment() { let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); + 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); + let aabb = segment.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(1.0, 0.5, 0.0)); assert_eq!(aabb.max, Vec3A::new(3.0, 1.5, 0.0)); - let bounding_sphere = segment.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = segment.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5)); + assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); } #[test] @@ -446,14 +454,18 @@ mod tests { Vec3::new(1.0, -1.0, -1.0), ]); let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = polyline.aabb_3d(translation, Quat::IDENTITY); + let aabb = polyline.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - let bounding_sphere = polyline.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = polyline.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(1.0).hypot(1.0)); + assert_eq!( + bounding_sphere.radius(), + ops::hypot(ops::hypot(1.0, 1.0), 1.0) + ); } #[test] @@ -461,25 +473,29 @@ mod tests { 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( + let aabb = cuboid.aabb_3d(Isometry3d::new( translation, Quat::from_rotation_z(std::f32::consts::FRAC_PI_4), - ); + )); let expected_half_size = Vec3A::new(1.0606601, 1.0606601, 0.5); assert_eq!(aabb.min, Vec3A::from(translation) - expected_half_size); assert_eq!(aabb.max, Vec3A::from(translation) + expected_half_size); - let bounding_sphere = cuboid.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = cuboid.bounding_sphere(Isometry3d::from_translation(translation)); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5).hypot(0.5)); + assert_eq!( + bounding_sphere.radius(), + ops::hypot(ops::hypot(1.0, 0.5), 0.5) + ); } #[test] fn cylinder() { let cylinder = Cylinder::new(0.5, 2.0); let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = cylinder.aabb_3d(translation, Quat::IDENTITY); + let aabb = cylinder.aabb_3d(isometry); assert_eq!( aabb.min, Vec3A::from(translation) - Vec3A::new(0.5, 1.0, 0.5) @@ -489,17 +505,18 @@ mod tests { Vec3A::from(translation) + Vec3A::new(0.5, 1.0, 0.5) ); - let bounding_sphere = cylinder.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = cylinder.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); - assert_eq!(bounding_sphere.radius(), 1.0_f32.hypot(0.5)); + assert_eq!(bounding_sphere.radius(), ops::hypot(1.0, 0.5)); } #[test] fn capsule() { let capsule = Capsule3d::new(0.5, 2.0); let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = capsule.aabb_3d(translation, Quat::IDENTITY); + let aabb = capsule.aabb_3d(isometry); assert_eq!( aabb.min, Vec3A::from(translation) - Vec3A::new(0.5, 1.5, 0.5) @@ -509,7 +526,7 @@ mod tests { Vec3A::from(translation) + Vec3A::new(0.5, 1.5, 0.5) ); - let bounding_sphere = capsule.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = capsule.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 1.5); } @@ -521,12 +538,13 @@ mod tests { height: 2.0, }; let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = cone.aabb_3d(translation, Quat::IDENTITY); + let aabb = cone.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - let bounding_sphere = cone.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = cone.bounding_sphere(isometry); assert_eq!( bounding_sphere.center, Vec3A::from(translation) + Vec3A::NEG_Y * 0.25 @@ -542,12 +560,13 @@ mod tests { height: 2.0, }; let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = conical_frustum.aabb_3d(translation, Quat::IDENTITY); + let aabb = conical_frustum.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(1.0, 0.0, -1.0)); assert_eq!(aabb.max, Vec3A::new(3.0, 2.0, 1.0)); - let bounding_sphere = conical_frustum.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = conical_frustum.bounding_sphere(isometry); assert_eq!( bounding_sphere.center, Vec3A::from(translation) + Vec3A::NEG_Y * 0.1875 @@ -563,14 +582,15 @@ mod tests { height: 1.0, }; let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = conical_frustum.aabb_3d(translation, Quat::IDENTITY); + let aabb = conical_frustum.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(-3.0, 0.5, -5.0)); assert_eq!(aabb.max, Vec3A::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); + let bounding_sphere = conical_frustum.bounding_sphere(isometry); assert_eq!( bounding_sphere.center, Vec3A::from(translation) + Vec3A::NEG_Y * 0.5 @@ -585,12 +605,13 @@ mod tests { major_radius: 1.0, }; let translation = Vec3::new(2.0, 1.0, 0.0); + let isometry = Isometry3d::from_translation(translation); - let aabb = torus.aabb_3d(translation, Quat::IDENTITY); + let aabb = torus.aabb_3d(isometry); assert_eq!(aabb.min, Vec3A::new(0.5, 0.5, -1.5)); assert_eq!(aabb.max, Vec3A::new(3.5, 1.5, 1.5)); - let bounding_sphere = torus.bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = torus.bounding_sphere(isometry); assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 1.5); } @@ -599,7 +620,7 @@ mod tests { fn triangle3d() { let zero_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::ZERO, Vec3::ZERO); - let br = zero_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY); + let br = zero_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); assert_eq!( br.center(), Vec3::ZERO.into(), @@ -611,7 +632,7 @@ mod tests { "incorrect bounding box half extents" ); - let bs = zero_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY); + let bs = zero_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); assert_eq!( bs.center, Vec3::ZERO.into(), @@ -620,14 +641,14 @@ mod tests { assert_eq!(bs.sphere.radius, 0.0, "incorrect bounding sphere radius"); let dup_degenerate_triangle = Triangle3d::new(Vec3::ZERO, Vec3::X, Vec3::X); - let bs = dup_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY); + let bs = dup_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); assert_eq!( bs.center, Vec3::new(0.5, 0.0, 0.0).into(), "incorrect bounding sphere center" ); assert_eq!(bs.sphere.radius, 0.5, "incorrect bounding sphere radius"); - let br = dup_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY); + let br = dup_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); assert_eq!( br.center(), Vec3::new(0.5, 0.0, 0.0).into(), @@ -640,14 +661,14 @@ mod tests { ); let collinear_degenerate_triangle = Triangle3d::new(Vec3::NEG_X, Vec3::ZERO, Vec3::X); - let bs = collinear_degenerate_triangle.bounding_sphere(Vec3::ZERO, Quat::IDENTITY); + let bs = collinear_degenerate_triangle.bounding_sphere(Isometry3d::IDENTITY); assert_eq!( bs.center, Vec3::ZERO.into(), "incorrect bounding sphere center" ); assert_eq!(bs.sphere.radius, 1.0, "incorrect bounding sphere radius"); - let br = collinear_degenerate_triangle.aabb_3d(Vec3::ZERO, Quat::IDENTITY); + let br = collinear_degenerate_triangle.aabb_3d(Isometry3d::IDENTITY); assert_eq!( br.center(), Vec3::ZERO.into(), diff --git a/crates/bevy_math/src/bounding/raycast2d.rs b/crates/bevy_math/src/bounding/raycast2d.rs index e3a4764725171e..745e53de8267cc 100644 --- a/crates/bevy_math/src/bounding/raycast2d.rs +++ b/crates/bevy_math/src/bounding/raycast2d.rs @@ -1,5 +1,5 @@ use super::{Aabb2d, BoundingCircle, IntersectsVolume}; -use crate::{Dir2, Ray2d, Vec2}; +use crate::{ops::FloatPow, Dir2, Ray2d, Vec2}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -76,8 +76,8 @@ impl RayCast2d { let offset = self.ray.origin - circle.center; let projected = offset.dot(*self.ray.direction); let closest_point = offset - projected * *self.ray.direction; - let distance_squared = circle.radius().powi(2) - closest_point.length_squared(); - if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared { + let distance_squared = circle.radius().squared() - closest_point.length_squared(); + if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared { None } else { let toi = -projected - distance_squared.sqrt(); diff --git a/crates/bevy_math/src/bounding/raycast3d.rs b/crates/bevy_math/src/bounding/raycast3d.rs index c69b790a79bf92..3a369d9fe142a1 100644 --- a/crates/bevy_math/src/bounding/raycast3d.rs +++ b/crates/bevy_math/src/bounding/raycast3d.rs @@ -1,5 +1,5 @@ use super::{Aabb3d, BoundingSphere, IntersectsVolume}; -use crate::{Dir3A, Ray3d, Vec3A}; +use crate::{ops::FloatPow, Dir3A, Ray3d, Vec3A}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -19,7 +19,9 @@ pub struct RayCast3d { } impl RayCast3d { - /// Construct a [`RayCast3d`] from an origin, [`Dir3A`], and max distance. + /// Construct a [`RayCast3d`] from an origin, [direction], and max distance. + /// + /// [direction]: crate::direction::Dir3 pub fn new(origin: impl Into, direction: impl Into, max: f32) -> Self { let direction = direction.into(); Self { @@ -71,8 +73,8 @@ impl RayCast3d { let offset = self.origin - sphere.center; let projected = offset.dot(*self.direction); let closest_point = offset - projected * *self.direction; - let distance_squared = sphere.radius().powi(2) - closest_point.length_squared(); - if distance_squared < 0. || projected.powi(2).copysign(-projected) < -distance_squared { + let distance_squared = sphere.radius().squared() - closest_point.length_squared(); + if distance_squared < 0. || projected.squared().copysign(-projected) < -distance_squared { None } else { let toi = -projected - distance_squared.sqrt(); @@ -108,7 +110,9 @@ pub struct AabbCast3d { } impl AabbCast3d { - /// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [`Dir3A`], and max distance. + /// Construct an [`AabbCast3d`] from an [`Aabb3d`], origin, [direction], and max distance. + /// + /// [direction]: crate::direction::Dir3 pub fn new( aabb: Aabb3d, origin: impl Into, @@ -151,7 +155,9 @@ pub struct BoundingSphereCast { } impl BoundingSphereCast { - /// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [`Dir3A`], and max distance. + /// Construct a [`BoundingSphereCast`] from a [`BoundingSphere`], origin, [direction], and max distance. + /// + /// [direction]: crate::direction::Dir3 pub fn new( sphere: BoundingSphere, origin: impl Into, diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index cc8869f6273101..525d8e403524e0 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,6 +1,6 @@ //! This module contains abstract mathematical traits shared by types used in `bevy_math`. -use crate::{Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; +use crate::{ops, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use std::fmt::Debug; use std::ops::{Add, Div, Mul, Neg, Sub}; @@ -256,7 +256,7 @@ pub trait StableInterpolate: Clone { /// object_position.smooth_nudge(&target_position, decay_rate, delta_time); /// ``` fn smooth_nudge(&mut self, target: &Self, decay_rate: f32, delta: f32) { - self.interpolate_stable_assign(target, 1.0 - f32::exp(-decay_rate * delta)); + self.interpolate_stable_assign(target, 1.0 - ops::exp(-decay_rate * delta)); } } diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 902f96fe5baaa3..b99c035c8e71fa 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -2,8 +2,9 @@ use std::{fmt::Debug, iter::once}; -use crate::{Vec2, VectorSpace}; +use crate::{ops::FloatPow, Vec2, VectorSpace}; +use itertools::Itertools; use thiserror::Error; #[cfg(feature = "bevy_reflect")] @@ -40,13 +41,13 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// vec2(5.0, 3.0), /// vec2(9.0, 8.0), /// ]]; -/// let bezier = CubicBezier::new(points).to_curve(); +/// let bezier = CubicBezier::new(points).to_curve().unwrap(); /// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// ``` #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicBezier { - /// The control points of the Bezier curve + /// The control points of the Bezier curve. pub control_points: Vec<[P; 4]>, } @@ -59,8 +60,10 @@ impl CubicBezier

{ } } impl CubicGenerator

for CubicBezier

{ + type Error = CubicBezierError; + #[inline] - fn to_curve(&self) -> CubicCurve

{ + fn to_curve(&self) -> Result, Self::Error> { // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin. // // See section 4.2 and equation 11. @@ -75,12 +78,22 @@ impl CubicGenerator

for CubicBezier

{ .control_points .iter() .map(|p| CubicSegment::coefficients(*p, char_matrix)) - .collect(); + .collect_vec(); - CubicCurve { segments } + if segments.is_empty() { + Err(CubicBezierError) + } else { + Ok(CubicCurve { segments }) + } } } +/// An error returned during cubic curve generation for cubic Bezier curves indicating that a +/// segment of control points was not present. +#[derive(Clone, Debug, Error)] +#[error("Unable to generate cubic curve: at least one set of control points is required")] +pub struct CubicBezierError; + /// A spline interpolated continuously between the nearest two control points, with the position and /// velocity of the curve specified at both control points. This curve passes through all control /// points, with the specified velocity which includes direction and parametric speed. @@ -95,8 +108,13 @@ impl CubicGenerator

for CubicBezier

{ /// Tangents are explicitly defined at each control point. /// /// ### Continuity -/// The curve is at minimum C0 continuous, meaning it has no holes or jumps. It is also C1, meaning the -/// tangent vector has no sudden jumps. +/// The curve is at minimum C1 continuous, meaning that it has no holes or jumps and the tangent vector also +/// has no sudden jumps. +/// +/// ### Parametrization +/// The first segment of the curve connects the first two control points, the second connects the second and +/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case +/// the final curve segment connects the last control point to the first. /// /// ### Usage /// @@ -114,13 +132,15 @@ impl CubicGenerator

for CubicBezier

{ /// vec2(0.0, 1.0), /// vec2(0.0, 1.0), /// ]; -/// let hermite = CubicHermite::new(points, tangents).to_curve(); +/// let hermite = CubicHermite::new(points, tangents).to_curve().unwrap(); /// let positions: Vec<_> = hermite.iter_positions(100).collect(); /// ``` +/// +/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicHermite { - /// The control points of the Hermite curve + /// The control points of the Hermite curve. pub control_points: Vec<(P, P)>, } impl CubicHermite

{ @@ -133,27 +153,69 @@ impl CubicHermite

{ control_points: control_points.into_iter().zip(tangents).collect(), } } -} -impl CubicGenerator

for CubicHermite

{ + + /// The characteristic matrix for this spline construction. + /// + /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear + /// combination of `p_i`, `v_i`, `p_{i+1}`, and `v_{i+1}`, where `(p_i, v_i)` and + /// `(p_{i+1}, v_{i+1})` are consecutive control points with tangents. #[inline] - fn to_curve(&self) -> CubicCurve

{ - let char_matrix = [ + fn char_matrix(&self) -> [[f32; 4]; 4] { + [ [1., 0., 0., 0.], [0., 1., 0., 0.], [-3., -2., 3., -1.], [2., 1., -2., 1.], - ]; + ] + } +} +impl CubicGenerator

for CubicHermite

{ + type Error = InsufficientDataError; + #[inline] + fn to_curve(&self) -> Result, Self::Error> { let segments = self .control_points .windows(2) .map(|p| { let (p0, v0, p1, v1) = (p[0].0, p[0].1, p[1].0, p[1].1); - CubicSegment::coefficients([p0, v0, p1, v1], char_matrix) + CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix()) }) - .collect(); + .collect_vec(); - CubicCurve { segments } + if segments.is_empty() { + Err(InsufficientDataError { + expected: 2, + given: self.control_points.len(), + }) + } else { + Ok(CubicCurve { segments }) + } + } +} +impl CyclicCubicGenerator

for CubicHermite

{ + type Error = InsufficientDataError; + + #[inline] + fn to_curve_cyclic(&self) -> Result, Self::Error> { + let segments = self + .control_points + .iter() + .circular_tuple_windows() + .map(|(&j0, &j1)| { + let (p0, v0, p1, v1) = (j0.0, j0.1, j1.0, j1.1); + CubicSegment::coefficients([p0, v0, p1, v1], self.char_matrix()) + }) + .collect_vec(); + + if segments.is_empty() { + Err(InsufficientDataError { + expected: 2, + given: self.control_points.len(), + }) + } else { + Ok(CubicCurve { segments }) + } } } @@ -173,6 +235,11 @@ impl CubicGenerator

for CubicHermite

{ /// The curve is at minimum C1, meaning that it is continuous (it has no holes or jumps), and its tangent /// vector is also well-defined everywhere, without sudden jumps. /// +/// ### Parametrization +/// The first segment of the curve connects the first two control points, the second connects the second and +/// third, and so on. This remains true when a cyclic curve is formed with [`to_curve_cyclic`], in which case +/// the final curve segment connects the last control point to the first. +/// /// ### Usage /// /// ``` @@ -183,9 +250,11 @@ impl CubicGenerator

for CubicHermite

{ /// vec2(5.0, 3.0), /// vec2(9.0, 8.0), /// ]; -/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve(); +/// let cardinal = CubicCardinalSpline::new(0.3, points).to_curve().unwrap(); /// let positions: Vec<_> = cardinal.iter_positions(100).collect(); /// ``` +/// +/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicCardinalSpline { @@ -211,23 +280,35 @@ impl CubicCardinalSpline

{ control_points: control_points.into(), } } -} -impl CubicGenerator

for CubicCardinalSpline

{ + + /// The characteristic matrix for this spline construction. + /// + /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear + /// combination of four consecutive control points. #[inline] - fn to_curve(&self) -> CubicCurve

{ + fn char_matrix(&self) -> [[f32; 4]; 4] { let s = self.tension; - let char_matrix = [ + [ [0., 1., 0., 0.], [-s, 0., s, 0.], [2. * s, s - 3., 3. - 2. * s, -s], [-s, 2. - s, s - 2., s], - ]; + ] + } +} +impl CubicGenerator

for CubicCardinalSpline

{ + type Error = InsufficientDataError; + #[inline] + fn to_curve(&self) -> Result, Self::Error> { let length = self.control_points.len(); // Early return to avoid accessing an invalid index if length < 2 { - return CubicCurve { segments: vec![] }; + return Err(InsufficientDataError { + expected: 2, + given: self.control_points.len(), + }); } // Extend the list of control points by mirroring the last second-to-last control points on each end; @@ -239,30 +320,78 @@ impl CubicGenerator

for CubicCardinalSpline

{ let mirrored_last = self.control_points[length - 1] * 2. - self.control_points[length - 2]; let extended_control_points = once(&mirrored_first) .chain(self.control_points.iter()) - .chain(once(&mirrored_last)) - .collect::>(); + .chain(once(&mirrored_last)); let segments = extended_control_points - .windows(4) - .map(|p| CubicSegment::coefficients([*p[0], *p[1], *p[2], *p[3]], char_matrix)) - .collect(); + .tuple_windows() + .map(|(&p0, &p1, &p2, &p3)| { + CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix()) + }) + .collect_vec(); - CubicCurve { segments } + Ok(CubicCurve { segments }) + } +} +impl CyclicCubicGenerator

for CubicCardinalSpline

{ + type Error = InsufficientDataError; + + #[inline] + fn to_curve_cyclic(&self) -> Result, Self::Error> { + let len = self.control_points.len(); + + if len < 2 { + return Err(InsufficientDataError { + expected: 2, + given: self.control_points.len(), + }); + } + + // This would ordinarily be the last segment, but we pick it out so that we can make it first + // in order to get a desirable parametrization where the first segment connects the first two + // control points instead of the second and third. + let first_segment = { + // We take the indices mod `len` in case `len` is very small. + let p0 = self.control_points[len - 1]; + let p1 = self.control_points[0]; + let p2 = self.control_points[1 % len]; + let p3 = self.control_points[2 % len]; + CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix()) + }; + + let later_segments = self + .control_points + .iter() + .circular_tuple_windows() + .map(|(&p0, &p1, &p2, &p3)| { + CubicSegment::coefficients([p0, p1, p2, p3], self.char_matrix()) + }) + .take(len - 1); + + let mut segments = Vec::with_capacity(len); + segments.push(first_segment); + segments.extend(later_segments); + + Ok(CubicCurve { segments }) } } /// A spline interpolated continuously across the nearest four control points. The curve does not -/// pass through any of the control points. +/// necessarily pass through any of the control points. /// /// ### Interpolation -/// The curve does not pass through control points. +/// The curve does not necessarily pass through its control points. /// /// ### Tangency -/// Tangents are automatically computed based on the position of control points. +/// Tangents are automatically computed based on the positions of control points. /// /// ### Continuity -/// The curve is C2 continuous, meaning it has no holes or jumps, and the tangent vector changes smoothly along -/// the entire curve length. The acceleration continuity of this spline makes it useful for camera paths. +/// The curve is C2 continuous, meaning it has no holes or jumps, the tangent vector changes smoothly along +/// the entire curve, and the acceleration also varies continuously. The acceleration continuity of this +/// spline makes it useful for camera paths. +/// +/// ### Parametrization +/// Each curve segment is defined by a window of four control points taken in sequence. When [`to_curve_cyclic`] +/// is used to form a cyclic curve, the three additional segments used to close the curve come last. /// /// ### Usage /// @@ -274,9 +403,11 @@ impl CubicGenerator

for CubicCardinalSpline

{ /// vec2(5.0, 3.0), /// vec2(9.0, 8.0), /// ]; -/// let b_spline = CubicBSpline::new(points).to_curve(); +/// let b_spline = CubicBSpline::new(points).to_curve().unwrap(); /// let positions: Vec<_> = b_spline.iter_positions(100).collect(); /// ``` +/// +/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicBSpline { @@ -290,10 +421,13 @@ impl CubicBSpline

{ control_points: control_points.into(), } } -} -impl CubicGenerator

for CubicBSpline

{ + + /// The characteristic matrix for this spline construction. + /// + /// Each row of this matrix expresses the coefficients of a [`CubicSegment`] as a linear + /// combination of four consecutive control points. #[inline] - fn to_curve(&self) -> CubicCurve

{ + fn char_matrix(&self) -> [[f32; 4]; 4] { // A derivation for this matrix can be found in "General Matrix Representations for B-splines" by Kaihuai Qin. // // See section 4.1 and equations 7 and 8. @@ -308,13 +442,56 @@ impl CubicGenerator

for CubicBSpline

{ .iter_mut() .for_each(|r| r.iter_mut().for_each(|c| *c /= 6.0)); + char_matrix + } +} +impl CubicGenerator

for CubicBSpline

{ + type Error = InsufficientDataError; + + #[inline] + fn to_curve(&self) -> Result, Self::Error> { let segments = self .control_points .windows(4) - .map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], char_matrix)) - .collect(); + .map(|p| CubicSegment::coefficients([p[0], p[1], p[2], p[3]], self.char_matrix())) + .collect_vec(); - CubicCurve { segments } + if segments.is_empty() { + Err(InsufficientDataError { + expected: 4, + given: self.control_points.len(), + }) + } else { + Ok(CubicCurve { segments }) + } + } +} + +impl CyclicCubicGenerator

for CubicBSpline

{ + type Error = InsufficientDataError; + + #[inline] + fn to_curve_cyclic(&self) -> Result, Self::Error> { + let segments = self + .control_points + .iter() + .circular_tuple_windows() + .map(|(&a, &b, &c, &d)| CubicSegment::coefficients([a, b, c, d], self.char_matrix())) + .collect_vec(); + + // Note that the parametrization is consistent with the one for `to_curve` but with + // the extra curve segments all tacked on at the end. This might be slightly counter-intuitive, + // since it means the first segment doesn't go "between" the first two control points, but + // between the second and third instead. + + if segments.is_empty() { + Err(InsufficientDataError { + expected: 2, + given: self.control_points.len(), + }) + } else { + Ok(CubicCurve { segments }) + } } } @@ -377,7 +554,7 @@ pub enum CubicNurbsError { /// When there is no knot multiplicity, the curve is C2 continuous, meaning it has no holes or jumps and the /// tangent vector changes smoothly along the entire curve length. Like the [`CubicBSpline`], the acceleration /// continuity makes it useful for camera paths. Knot multiplicity of 2 in intermediate knots reduces the -/// continuity to C2, and knot multiplicity of 3 reduces the continuity to C0. The curve is always at least +/// continuity to C1, and knot multiplicity of 3 reduces the continuity to C0. The curve is always at least /// C0, meaning it has no jumps or holes. /// /// ### Usage @@ -394,7 +571,8 @@ pub enum CubicNurbsError { /// let knots = [0.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0]; /// let nurbs = CubicNurbs::new(points, Some(weights), Some(knots)) /// .expect("NURBS construction failed!") -/// .to_curve(); +/// .to_curve() +/// .unwrap(); /// let positions: Vec<_> = nurbs.iter_positions(100).collect(); /// ``` #[derive(Clone, Debug)] @@ -553,12 +731,12 @@ impl CubicNurbs

{ // t[5] := t_i+2 // t[6] := t_i+3 - let m00 = (t[4] - t[3]).powi(2) / ((t[4] - t[2]) * (t[4] - t[1])); - let m02 = (t[3] - t[2]).powi(2) / ((t[5] - t[2]) * (t[4] - t[2])); + let m00 = (t[4] - t[3]).squared() / ((t[4] - t[2]) * (t[4] - t[1])); + let m02 = (t[3] - t[2]).squared() / ((t[5] - t[2]) * (t[4] - t[2])); let m12 = (3.0 * (t[4] - t[3]) * (t[3] - t[2])) / ((t[5] - t[2]) * (t[4] - t[2])); - let m22 = 3.0 * (t[4] - t[3]).powi(2) / ((t[5] - t[2]) * (t[4] - t[2])); - let m33 = (t[4] - t[3]).powi(2) / ((t[6] - t[3]) * (t[5] - t[3])); - let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).powi(2) / ((t[5] - t[3]) * (t[5] - t[2])); + let m22 = 3.0 * (t[4] - t[3]).squared() / ((t[5] - t[2]) * (t[4] - t[2])); + let m33 = (t[4] - t[3]).squared() / ((t[6] - t[3]) * (t[5] - t[3])); + let m32 = -m22 / 3.0 - m33 - (t[4] - t[3]).squared() / ((t[5] - t[3]) * (t[5] - t[2])); [ [m00, 1.0 - m00 - m02, m02, 0.0], [-3.0 * m00, 3.0 * m00 - m12, m12, 0.0], @@ -568,8 +746,10 @@ impl CubicNurbs

{ } } impl RationalGenerator

for CubicNurbs

{ + type Error = InsufficientDataError; + #[inline] - fn to_curve(&self) -> RationalCurve

{ + fn to_curve(&self) -> Result, Self::Error> { let segments = self .control_points .windows(4) @@ -591,8 +771,15 @@ impl RationalGenerator

for CubicNurbs

{ matrix, ) }) - .collect(); - RationalCurve { segments } + .collect_vec(); + if segments.is_empty() { + Err(InsufficientDataError { + expected: 4, + given: self.control_points.len(), + }) + } else { + Ok(RationalCurve { segments }) + } } } @@ -606,14 +793,20 @@ impl RationalGenerator

for CubicNurbs

{ /// /// ### Continuity /// The curve is C0 continuous, meaning it has no holes or jumps. +/// +/// ### Parametrization +/// Each curve segment connects two adjacent control points in sequence. When a cyclic curve is +/// formed with [`to_curve_cyclic`], the final segment connects the last control point with the first. +/// +/// [`to_curve_cyclic`]: CyclicCubicGenerator::to_curve_cyclic #[derive(Clone, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct LinearSpline { - /// The control points of the NURBS + /// The control points of the linear spline. pub points: Vec

, } impl LinearSpline

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

{ } } impl CubicGenerator

for LinearSpline

{ + type Error = InsufficientDataError; + #[inline] - fn to_curve(&self) -> CubicCurve

{ + fn to_curve(&self) -> Result, Self::Error> { let segments = self .points .windows(2) @@ -633,15 +828,70 @@ impl CubicGenerator

for LinearSpline

{ coeff: [a, b - a, P::default(), P::default()], } }) - .collect(); - CubicCurve { segments } + .collect_vec(); + + if segments.is_empty() { + Err(InsufficientDataError { + expected: 2, + given: self.points.len(), + }) + } else { + Ok(CubicCurve { segments }) + } + } +} +impl CyclicCubicGenerator

for LinearSpline

{ + type Error = InsufficientDataError; + + #[inline] + fn to_curve_cyclic(&self) -> Result, Self::Error> { + let segments = self + .points + .iter() + .circular_tuple_windows() + .map(|(&a, &b)| CubicSegment { + coeff: [a, b - a, P::default(), P::default()], + }) + .collect_vec(); + + if segments.is_empty() { + Err(InsufficientDataError { + expected: 2, + given: self.points.len(), + }) + } else { + Ok(CubicCurve { segments }) + } } } +/// An error indicating that a spline construction didn't have enough control points to generate a curve. +#[derive(Clone, Debug, Error)] +#[error("Not enough data to build curve: needed at least {expected} control points but was only given {given}")] +pub struct InsufficientDataError { + expected: usize, + given: usize, +} + /// Implement this on cubic splines that can generate a cubic curve from their spline parameters. pub trait CubicGenerator { + /// An error type indicating why construction might fail. + type Error; + /// Build a [`CubicCurve`] by computing the interpolation coefficients for each curve segment. - fn to_curve(&self) -> CubicCurve

; + fn to_curve(&self) -> Result, Self::Error>; +} + +/// Implement this on cubic splines that can generate a cyclic cubic curve from their spline parameters. +/// +/// This makes sense only when the control data can be interpreted cyclically. +pub trait CyclicCubicGenerator { + /// An error type indicating why construction might fail. + type Error; + + /// Build a cyclic [`CubicCurve`] by computing the interpolation coefficients for each curve segment, + /// treating the control data as cyclic so that the result is a closed curve. + fn to_curve_cyclic(&self) -> Result, Self::Error>; } /// A segment of a cubic curve, used to hold precomputed coefficients for fast interpolation. @@ -651,7 +901,7 @@ pub trait CubicGenerator { #[derive(Copy, Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Default))] pub struct CubicSegment { - /// Coefficients of the segment + /// Polynomial coefficients for the segment. pub coeff: [P; 4], } @@ -710,7 +960,9 @@ impl CubicSegment { /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. pub fn new_bezier(p1: impl Into, p2: impl Into) -> Self { let (p0, p3) = (Vec2::ZERO, Vec2::ONE); - let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve(); + let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]) + .to_curve() + .unwrap(); // Succeeds because resulting curve is guaranteed to have one segment bezier.segments[0] } @@ -811,11 +1063,22 @@ impl CubicSegment { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct CubicCurve { - /// Segments of the curve - pub segments: Vec>, + /// The segments comprising the curve. This must always be nonempty. + segments: Vec>, } impl CubicCurve

{ + /// Create a new curve from a collection of segments. If the collection of segments is empty, + /// a curve cannot be built and `None` will be returned instead. + pub fn from_segments(segments: impl Into>>) -> Option { + let segments: Vec<_> = segments.into(); + if segments.is_empty() { + None + } else { + Some(Self { segments }) + } + } + /// Compute the position of a point on the cubic curve at the parametric value `t`. /// /// Note that `t` varies from `0..=(n_points - 3)`. @@ -934,8 +1197,11 @@ impl IntoIterator for CubicCurve

{ /// Implement this on cubic splines that can generate a rational cubic curve from their spline parameters. pub trait RationalGenerator { + /// An error type indicating why construction might fail. + type Error; + /// Build a [`RationalCurve`] by computing the interpolation coefficients for each curve segment. - fn to_curve(&self) -> RationalCurve

; + fn to_curve(&self) -> Result, Self::Error>; } /// A segment of a rational cubic curve, used to hold precomputed coefficients for fast interpolation. @@ -988,7 +1254,7 @@ impl RationalSegment

{ // Position = N/D therefore // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2 numerator_derivative / denominator - - numerator * (denominator_derivative / denominator.powi(2)) + - numerator * (denominator_derivative / denominator.squared()) } /// Instantaneous acceleration of a point at parametric value `t` in `[0, knot_span)`. @@ -1022,10 +1288,10 @@ impl RationalSegment

{ // Velocity = (N/D)' = N'/D - N * D'/D^2 = (N' * D - N * D')/D^2 // Acceleration = (N/D)'' = ((N' * D - N * D')/D^2)' = N''/D + N' * (-2D'/D^2) + N * (-D''/D^2 + 2D'^2/D^3) numerator_second_derivative / denominator - + numerator_derivative * (-2.0 * denominator_derivative / denominator.powi(2)) + + numerator_derivative * (-2.0 * denominator_derivative / denominator.squared()) + numerator - * (-denominator_second_derivative / denominator.powi(2) - + 2.0 * denominator_derivative.powi(2) / denominator.powi(3)) + * (-denominator_second_derivative / denominator.squared() + + 2.0 * denominator_derivative.squared() / denominator.cubed()) } /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix. @@ -1073,11 +1339,22 @@ impl RationalSegment

{ #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct RationalCurve { - /// The segments in the curve - pub segments: Vec>, + /// The segments comprising the curve. This must always be nonempty. + segments: Vec>, } impl RationalCurve

{ + /// Create a new curve from a collection of segments. If the collection of segments is empty, + /// a curve cannot be built and `None` will be returned instead. + pub fn from_segments(segments: impl Into>>) -> Option { + let segments: Vec<_> = segments.into(); + if segments.is_empty() { + None + } else { + Some(Self { segments }) + } + } + /// Compute the position of a point on the curve at the parametric value `t`. /// /// Note that `t` varies from `0..=(n_points - 3)`. @@ -1235,9 +1512,12 @@ impl From> for RationalCurve

{ mod tests { use glam::{vec2, Vec2}; - use crate::cubic_splines::{ - CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve, - RationalGenerator, + use crate::{ + cubic_splines::{ + CubicBSpline, CubicBezier, CubicGenerator, CubicNurbs, CubicSegment, RationalCurve, + RationalGenerator, + }, + ops::{self, FloatPow}, }; /// How close two floats can be and still be considered equal @@ -1253,7 +1533,7 @@ mod tests { vec2(5.0, 3.0), vec2(9.0, 8.0), ]]; - let bezier = CubicBezier::new(points).to_curve(); + let bezier = CubicBezier::new(points).to_curve().unwrap(); for i in 0..=N_SAMPLES { let t = i as f32 / N_SAMPLES as f32; // Check along entire length assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ); @@ -1263,10 +1543,10 @@ mod tests { /// Manual, hardcoded function for computing the position along a cubic bezier. fn cubic_manual(t: f32, points: [Vec2; 4]) -> Vec2 { let p = points; - p[0] * (1.0 - t).powi(3) - + 3.0 * p[1] * t * (1.0 - t).powi(2) - + 3.0 * p[2] * t.powi(2) * (1.0 - t) - + p[3] * t.powi(3) + p[0] * (1.0 - t).cubed() + + 3.0 * p[1] * t * (1.0 - t).squared() + + 3.0 * p[2] * t.squared() * (1.0 - t) + + p[3] * t.cubed() } /// Basic cubic Bezier easing test to verify the shape of the curve. @@ -1310,7 +1590,9 @@ mod tests { let tension = 0.2; let [p0, p1, p2, p3] = [vec2(-1., -2.), vec2(0., 1.), vec2(1., 2.), vec2(-2., 1.)]; - let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3]).to_curve(); + let curve = CubicCardinalSpline::new(tension, [p0, p1, p2, p3]) + .to_curve() + .unwrap(); // Positions at segment endpoints assert!(curve.position(0.).abs_diff_eq(p0, FLOAT_EQ)); @@ -1348,7 +1630,7 @@ mod tests { vec2(0.0, 0.0), ]; - let b_spline = CubicBSpline::new(points).to_curve(); + let b_spline = CubicBSpline::new(points).to_curve().unwrap(); let rational_b_spline = RationalCurve::from(b_spline.clone()); /// Tests if two vectors of points are approximately the same @@ -1395,8 +1677,8 @@ mod tests { // subjecting ones self to a lot of tedious matrix algebra. let alpha = FRAC_PI_2; - let leg = 2.0 * f32::sin(alpha / 2.0) / (1.0 + 2.0 * f32::cos(alpha / 2.0)); - let weight = (1.0 + 2.0 * f32::cos(alpha / 2.0)) / 3.0; + let leg = 2.0 * ops::sin(alpha / 2.0) / (1.0 + 2.0 * ops::cos(alpha / 2.0)); + let weight = (1.0 + 2.0 * ops::cos(alpha / 2.0)) / 3.0; let points = [ vec2(1.0, 0.0), vec2(1.0, leg), @@ -1406,7 +1688,7 @@ mod tests { let weights = [1.0, weight, weight, 1.0]; let knots = [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]; let spline = CubicNurbs::new(points, Some(weights), Some(knots)).unwrap(); - let curve = spline.to_curve(); + let curve = spline.to_curve().unwrap(); for (i, point) in curve.iter_positions(10).enumerate() { assert!( f32::abs(point.length() - 1.0) < EPSILON, diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs new file mode 100644 index 00000000000000..92ad2fdc71a3c1 --- /dev/null +++ b/crates/bevy_math/src/curve/cores.rs @@ -0,0 +1,628 @@ +//! Core data structures to be used internally in Curve implementations, encapsulating storage +//! and access patterns for reuse. +//! +//! The `Core` types here expose their fields publicly so that it is easier to manipulate and +//! extend them, but in doing so, you must maintain the invariants of those fields yourself. The +//! provided methods all maintain the invariants, so this is only a concern if you manually mutate +//! the fields. + +use super::interval::Interval; +use core::fmt::Debug; +use itertools::Itertools; +use thiserror::Error; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + +/// This type expresses the relationship of a value to a fixed collection of values. It is a kind +/// of summary used intermediately by sampling operations. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub enum InterpolationDatum { + /// This value lies exactly on a value in the family. + Exact(T), + + /// This value is off the left tail of the family; the inner value is the family's leftmost. + LeftTail(T), + + /// This value is off the right tail of the family; the inner value is the family's rightmost. + RightTail(T), + + /// This value lies on the interior, in between two points, with a third parameter expressing + /// the interpolation factor between the two. + Between(T, T, f32), +} + +impl InterpolationDatum { + /// Map all values using a given function `f`, leaving the interpolation parameters in any + /// [`Between`] variants unchanged. + /// + /// [`Between`]: `InterpolationDatum::Between` + #[must_use] + pub fn map(self, f: impl Fn(T) -> S) -> InterpolationDatum { + match self { + InterpolationDatum::Exact(v) => InterpolationDatum::Exact(f(v)), + InterpolationDatum::LeftTail(v) => InterpolationDatum::LeftTail(f(v)), + InterpolationDatum::RightTail(v) => InterpolationDatum::RightTail(f(v)), + InterpolationDatum::Between(u, v, s) => InterpolationDatum::Between(f(u), f(v), s), + } + } +} + +/// The data core of a curve derived from evenly-spaced samples. The intention is to use this +/// in addition to explicit or inferred interpolation information in user-space in order to +/// implement curves using [`domain`] and [`sample_with`]. +/// +/// The internals are made transparent to give curve authors freedom, but [the provided constructor] +/// enforces the required invariants, and the methods maintain those invariants. +/// +/// [the provided constructor]: EvenCore::new +/// [`domain`]: EvenCore::domain +/// [`sample_with`]: EvenCore::sample_with +/// +/// # Example +/// ```rust +/// # use bevy_math::curve::*; +/// # use bevy_math::curve::cores::*; +/// // Let's make a curve that interpolates evenly spaced samples using either linear interpolation +/// // or step "interpolation" — i.e. just using the most recent sample as the source of truth. +/// enum InterpolationMode { +/// Linear, +/// Step, +/// } +/// +/// // Linear interpolation mode is driven by a trait. +/// trait LinearInterpolate { +/// fn lerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// // Step interpolation just uses an explicit function. +/// fn step(first: &T, second: &T, t: f32) -> T { +/// if t >= 1.0 { +/// second.clone() +/// } else { +/// first.clone() +/// } +/// } +/// +/// // Omitted: Implementing `LinearInterpolate` on relevant types; e.g. `f32`, `Vec3`, and so on. +/// +/// // The curve itself uses `EvenCore` to hold the evenly-spaced samples, and the `sample_with` +/// // function will do all the work of interpolating once given a function to do it with. +/// struct MyCurve { +/// core: EvenCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for MyCurve +/// where +/// T: LinearInterpolate + Clone, +/// { +/// fn domain(&self) -> Interval { +/// self.core.domain() +/// } +/// +/// fn sample_unchecked(&self, t: f32) -> T { +/// // To sample this curve, check the interpolation mode and dispatch accordingly. +/// match self.interpolation_mode { +/// InterpolationMode::Linear => self.core.sample_with(t, ::lerp), +/// InterpolationMode::Step => self.core.sample_with(t, step), +/// } +/// } +/// } +/// ``` +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct EvenCore { + /// The domain over which the samples are taken, which corresponds to the domain of the curve + /// formed by interpolating them. + /// + /// # Invariants + /// This must always be a bounded interval; i.e. its endpoints must be finite. + pub domain: Interval, + + /// The samples that are interpolated to extract values. + /// + /// # Invariants + /// This must always have a length of at least 2. + pub samples: Vec, +} + +/// An error indicating that an [`EvenCore`] could not be constructed. +#[derive(Debug, Error)] +#[error("Could not construct an EvenCore")] +pub enum EvenCoreError { + /// Not enough samples were provided. + #[error("Need at least two samples to create an EvenCore, but {samples} were provided")] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, + + /// Unbounded domains are not compatible with `EvenCore`. + #[error("Cannot create a EvenCore over an unbounded domain")] + UnboundedDomain, +} + +impl EvenCore { + /// Create a new [`EvenCore`] from the specified `domain` and `samples`. The samples are + /// regarded to be evenly spaced within the given domain interval, so that the outermost + /// samples form the boundary of that interval. An error is returned if there are not at + /// least 2 samples or if the given domain is unbounded. + #[inline] + pub fn new( + domain: Interval, + samples: impl IntoIterator, + ) -> Result { + let samples: Vec = samples.into_iter().collect(); + if samples.len() < 2 { + return Err(EvenCoreError::NotEnoughSamples { + samples: samples.len(), + }); + } + if !domain.is_bounded() { + return Err(EvenCoreError::UnboundedDomain); + } + + Ok(EvenCore { domain, samples }) + } + + /// The domain of the curve derived from this core. + #[inline] + pub const fn domain(&self) -> Interval { + self.domain + } + + /// Obtain a value from the held samples using the given `interpolation` to interpolate + /// between adjacent samples. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + #[inline] + pub fn sample_with(&self, t: f32, interpolation: I) -> T + where + T: Clone, + I: Fn(&T, &T, f32) -> T, + { + match even_interp(self.domain, self.samples.len(), t) { + InterpolationDatum::Exact(idx) + | InterpolationDatum::LeftTail(idx) + | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(), + InterpolationDatum::Between(lower_idx, upper_idx, s) => { + interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) + } + } + } + + /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover + /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can + /// be used to interpolate between the two contained values with the given parameter. The other + /// variants give additional context about where the value is relative to the family of samples. + /// + /// [`Between`]: `InterpolationDatum::Between` + pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { + even_interp(self.domain, self.samples.len(), t).map(|idx| &self.samples[idx]) + } + + /// Like [`sample_interp`], but the returned values include the sample times. This can be + /// useful when sample interpolation is not scale-invariant. + /// + /// [`sample_interp`]: EvenCore::sample_interp + pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { + let segment_len = self.domain.length() / (self.samples.len() - 1) as f32; + even_interp(self.domain, self.samples.len(), t).map(|idx| { + ( + self.domain.start() + segment_len * idx as f32, + &self.samples[idx], + ) + }) + } +} + +/// Given a domain and a number of samples taken over that interval, return an [`InterpolationDatum`] +/// that governs how samples are extracted relative to the stored data. +/// +/// `domain` must be a bounded interval (i.e. `domain.is_bounded() == true`). +/// +/// `samples` must be at least 2. +/// +/// This function will never panic, but it may return invalid indices if its assumptions are violated. +pub fn even_interp(domain: Interval, samples: usize, t: f32) -> InterpolationDatum { + let subdivs = samples - 1; + let step = domain.length() / subdivs as f32; + let t_shifted = t - domain.start(); + let steps_taken = t_shifted / step; + + if steps_taken <= 0.0 { + // To the left side of all the samples. + InterpolationDatum::LeftTail(0) + } else if steps_taken >= subdivs as f32 { + // To the right side of all the samples + InterpolationDatum::RightTail(samples - 1) + } else { + let lower_index = steps_taken.floor() as usize; + // This upper index is always valid because `steps_taken` is a finite value + // strictly less than `samples - 1`, so its floor is at most `samples - 2` + let upper_index = lower_index + 1; + let s = steps_taken.fract(); + InterpolationDatum::Between(lower_index, upper_index, s) + } +} + +/// The data core of a curve defined by unevenly-spaced samples or keyframes. The intention is to +/// use this in concert with implicitly or explicitly-defined interpolation in user-space in +/// order to implement the curve interface using [`domain`] and [`sample_with`]. +/// +/// The internals are made transparent to give curve authors freedom, but [the provided constructor] +/// enforces the required invariants, and the methods maintain those invariants. +/// +/// # Example +/// ```rust +/// # use bevy_math::curve::*; +/// # use bevy_math::curve::cores::*; +/// // Let's make a curve formed by interpolating rotations. +/// // We'll support two common modes of interpolation: +/// // - Normalized linear: First do linear interpolation, then normalize to get a valid rotation. +/// // - Spherical linear: Interpolate through valid rotations with constant angular velocity. +/// enum InterpolationMode { +/// NormalizedLinear, +/// SphericalLinear, +/// } +/// +/// // Our interpolation modes will be driven by traits. +/// trait NormalizedLinearInterpolate { +/// fn nlerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// trait SphericalLinearInterpolate { +/// fn slerp(&self, other: &Self, t: f32) -> Self; +/// } +/// +/// // Omitted: These traits would be implemented for `Rot2`, `Quat`, and other rotation representations. +/// +/// // The curve itself just needs to use the curve core for keyframes, `UnevenCore`, which handles +/// // everything except for the explicit interpolation used. +/// struct RotationCurve { +/// core: UnevenCore, +/// interpolation_mode: InterpolationMode, +/// } +/// +/// impl Curve for RotationCurve +/// where +/// T: NormalizedLinearInterpolate + SphericalLinearInterpolate + Clone, +/// { +/// fn domain(&self) -> Interval { +/// self.core.domain() +/// } +/// +/// fn sample_unchecked(&self, t: f32) -> T { +/// // To sample the curve, we just look at the interpolation mode and +/// // dispatch accordingly. +/// match self.interpolation_mode { +/// InterpolationMode::NormalizedLinear => +/// self.core.sample_with(t, ::nlerp), +/// InterpolationMode::SphericalLinear => +/// self.core.sample_with(t, ::slerp), +/// } +/// } +/// } +/// ``` +/// +/// [`domain`]: UnevenCore::domain +/// [`sample_with`]: UnevenCore::sample_with +/// [the provided constructor]: UnevenCore::new +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct UnevenCore { + /// The times for the samples of this curve. + /// + /// # Invariants + /// This must always have a length of at least 2, be sorted, and have no + /// duplicated or non-finite times. + pub times: Vec, + + /// The samples corresponding to the times for this curve. + /// + /// # Invariants + /// This must always have the same length as `times`. + pub samples: Vec, +} + +/// An error indicating that an [`UnevenCore`] could not be constructed. +#[derive(Debug, Error)] +#[error("Could not construct an UnevenCore")] +pub enum UnevenCoreError { + /// Not enough samples were provided. + #[error( + "Need at least two unique samples to create an UnevenCore, but {samples} were provided" + )] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, +} + +impl UnevenCore { + /// Create a new [`UnevenCore`]. The given samples are filtered to finite times and + /// sorted internally; if there are not at least 2 valid timed samples, an error will be + /// returned. + pub fn new(timed_samples: impl IntoIterator) -> Result { + // Filter out non-finite sample times first so they don't interfere with sorting/deduplication. + let mut timed_samples = timed_samples + .into_iter() + .filter(|(t, _)| t.is_finite()) + .collect_vec(); + timed_samples + // Using `total_cmp` is fine because no NANs remain and because deduplication uses + // `PartialEq` anyway (so -0.0 and 0.0 will be considered equal later regardless). + .sort_by(|(t0, _), (t1, _)| t0.total_cmp(t1)); + timed_samples.dedup_by_key(|(t, _)| *t); + + if timed_samples.len() < 2 { + return Err(UnevenCoreError::NotEnoughSamples { + samples: timed_samples.len(), + }); + } + + let (times, samples): (Vec, Vec) = timed_samples.into_iter().unzip(); + Ok(UnevenCore { times, samples }) + } + + /// The domain of the curve derived from this core. + /// + /// # Panics + /// This method may panic if the type's invariants aren't satisfied. + #[inline] + pub fn domain(&self) -> Interval { + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + Interval::new(*start, *end).unwrap() + } + + /// Obtain a value from the held samples using the given `interpolation` to interpolate + /// between adjacent samples. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + #[inline] + pub fn sample_with(&self, t: f32, interpolation: I) -> T + where + T: Clone, + I: Fn(&T, &T, f32) -> T, + { + match uneven_interp(&self.times, t) { + InterpolationDatum::Exact(idx) + | InterpolationDatum::LeftTail(idx) + | InterpolationDatum::RightTail(idx) => self.samples[idx].clone(), + InterpolationDatum::Between(lower_idx, upper_idx, s) => { + interpolation(&self.samples[lower_idx], &self.samples[upper_idx], s) + } + } + } + + /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover + /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can + /// be used to interpolate between the two contained values with the given parameter. The other + /// variants give additional context about where the value is relative to the family of samples. + /// + /// [`Between`]: `InterpolationDatum::Between` + pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&T> { + uneven_interp(&self.times, t).map(|idx| &self.samples[idx]) + } + + /// Like [`sample_interp`], but the returned values include the sample times. This can be + /// useful when sample interpolation is not scale-invariant. + /// + /// [`sample_interp`]: UnevenCore::sample_interp + pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &T)> { + uneven_interp(&self.times, t).map(|idx| (self.times[idx], &self.samples[idx])) + } + + /// This core, but with the sample times moved by the map `f`. + /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// but the function inputs to each are inverses of one another. + /// + /// The samples are re-sorted by time after mapping and deduplicated by output time, so + /// the function `f` should generally be injective over the set of sample times, otherwise + /// data will be deleted. + /// + /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize + #[must_use] + pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { + let mut timed_samples = self + .times + .into_iter() + .map(f) + .zip(self.samples) + .collect_vec(); + timed_samples.sort_by(|(t1, _), (t2, _)| t1.total_cmp(t2)); + timed_samples.dedup_by_key(|(t, _)| *t); + (self.times, self.samples) = timed_samples.into_iter().unzip(); + self + } +} + +/// The data core of a curve using uneven samples (i.e. keyframes), where each sample time +/// yields some fixed number of values — the [sampling width]. This may serve as storage for +/// curves that yield vectors or iterators, and in some cases, it may be useful for cache locality +/// if the sample type can effectively be encoded as a fixed-length slice of values. +/// +/// [sampling width]: ChunkedUnevenCore::width +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ChunkedUnevenCore { + /// The times, one for each sample. + /// + /// # Invariants + /// This must always have a length of at least 2, be sorted, and have no duplicated or + /// non-finite times. + pub times: Vec, + + /// The values that are used in sampling. Each width-worth of these correspond to a single sample. + /// + /// # Invariants + /// The length of this vector must always be some fixed integer multiple of that of `times`. + pub values: Vec, +} + +/// An error that indicates that a [`ChunkedUnevenCore`] could not be formed. +#[derive(Debug, Error)] +#[error("Could not create a ChunkedUnevenCore")] +pub enum ChunkedUnevenCoreError { + /// The width of a `ChunkedUnevenCore` cannot be zero. + #[error("Chunk width must be at least 1")] + ZeroWidth, + + /// At least two sample times are necessary to interpolate in `ChunkedUnevenCore`. + #[error( + "Need at least two unique samples to create a ChunkedUnevenCore, but {samples} were provided" + )] + NotEnoughSamples { + /// The number of samples that were provided. + samples: usize, + }, + + /// The length of the value buffer is supposed to be the `width` times the number of samples. + #[error("Expected {expected} total values based on width, but {actual} were provided")] + MismatchedLengths { + /// The expected length of the value buffer. + expected: usize, + /// The actual length of the value buffer. + actual: usize, + }, +} + +impl ChunkedUnevenCore { + /// Create a new [`ChunkedUnevenCore`]. The given `times` are sorted, filtered to finite times, + /// and deduplicated. See the [type-level documentation] for more information about this type. + /// + /// Produces an error in any of the following circumstances: + /// - `width` is zero. + /// - `times` has less than `2` valid unique entries. + /// - `values` has the incorrect length relative to `times`. + /// + /// [type-level documentation]: ChunkedUnevenCore + pub fn new( + times: impl Into>, + values: impl Into>, + width: usize, + ) -> Result { + let times: Vec = times.into(); + let values: Vec = values.into(); + + if width == 0 { + return Err(ChunkedUnevenCoreError::ZeroWidth); + } + + let times = filter_sort_dedup_times(times); + + if times.len() < 2 { + return Err(ChunkedUnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + + if values.len() != times.len() * width { + return Err(ChunkedUnevenCoreError::MismatchedLengths { + expected: times.len() * width, + actual: values.len(), + }); + } + + Ok(Self { times, values }) + } + + /// The domain of the curve derived from this core. + /// + /// # Panics + /// This may panic if this type's invariants aren't met. + #[inline] + pub fn domain(&self) -> Interval { + let start = self.times.first().unwrap(); + let end = self.times.last().unwrap(); + Interval::new(*start, *end).unwrap() + } + + /// The sample width: the number of values that are contained in each sample. + #[inline] + pub fn width(&self) -> usize { + self.values.len() / self.times.len() + } + + /// Given a time `t`, obtain a [`InterpolationDatum`] which governs how interpolation might recover + /// a sample at time `t`. For example, when a [`Between`] value is returned, its contents can + /// be used to interpolate between the two contained values with the given parameter. The other + /// variants give additional context about where the value is relative to the family of samples. + /// + /// [`Between`]: `InterpolationDatum::Between` + #[inline] + pub fn sample_interp(&self, t: f32) -> InterpolationDatum<&[T]> { + uneven_interp(&self.times, t).map(|idx| self.time_index_to_slice(idx)) + } + + /// Like [`sample_interp`], but the returned values include the sample times. This can be + /// useful when sample interpolation is not scale-invariant. + /// + /// [`sample_interp`]: ChunkedUnevenCore::sample_interp + pub fn sample_interp_timed(&self, t: f32) -> InterpolationDatum<(f32, &[T])> { + uneven_interp(&self.times, t).map(|idx| (self.times[idx], self.time_index_to_slice(idx))) + } + + /// Given an index in [times], returns the slice of [values] that correspond to the sample at + /// that time. + /// + /// [times]: ChunkedUnevenCore::times + /// [values]: ChunkedUnevenCore::values + #[inline] + fn time_index_to_slice(&self, idx: usize) -> &[T] { + let width = self.width(); + let lower_idx = width * idx; + let upper_idx = lower_idx + width; + &self.values[lower_idx..upper_idx] + } +} + +/// Sort the given times, deduplicate them, and filter them to only finite times. +fn filter_sort_dedup_times(times: impl IntoIterator) -> Vec { + // Filter before sorting/deduplication so that NAN doesn't interfere with them. + let mut times = times.into_iter().filter(|t| t.is_finite()).collect_vec(); + times.sort_by(f32::total_cmp); + times.dedup(); + times +} + +/// Given a list of `times` and a target value, get the interpolation relationship for the +/// target value in terms of the indices of the starting list. In a sense, this encapsulates the +/// heart of uneven/keyframe sampling. +/// +/// `times` is assumed to be sorted, deduplicated, and consisting only of finite values. It is also +/// assumed to contain at least two values. +/// +/// # Panics +/// This function will panic if `times` contains NAN. +pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum { + match times.binary_search_by(|pt| pt.partial_cmp(&t).unwrap()) { + Ok(index) => InterpolationDatum::Exact(index), + Err(index) => { + if index == 0 { + // This is before the first keyframe. + InterpolationDatum::LeftTail(0) + } else if index >= times.len() { + // This is after the last keyframe. + InterpolationDatum::RightTail(times.len() - 1) + } else { + // This is actually in the middle somewhere. + let t_lower = times[index - 1]; + let t_upper = times[index]; + let s = (t - t_lower) / (t_upper - t_lower); + InterpolationDatum::Between(index - 1, index, s) + } + } + } +} diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs new file mode 100644 index 00000000000000..3ac30de4a6aaca --- /dev/null +++ b/crates/bevy_math/src/curve/interval.rs @@ -0,0 +1,373 @@ +//! The [`Interval`] type for nonempty intervals used by the [`Curve`](super::Curve) trait. + +use itertools::Either; +use std::{ + cmp::{max_by, min_by}, + ops::RangeInclusive, +}; +use thiserror::Error; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// A nonempty closed interval, possibly unbounded in either direction. +/// +/// In other words, the interval may stretch all the way to positive or negative infinity, but it +/// will always have some nonempty interior. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Interval { + start: f32, + end: f32, +} + +/// An error that indicates that an operation would have returned an invalid [`Interval`]. +#[derive(Debug, Error)] +#[error("The resulting interval would be invalid (empty or with a NaN endpoint)")] +pub struct InvalidIntervalError; + +/// An error indicating that spaced points could not be extracted from an unbounded interval. +#[derive(Debug, Error)] +#[error("Cannot extract spaced points from an unbounded interval")] +pub struct SpacedPointsError; + +/// An error indicating that a linear map between intervals could not be constructed because of +/// unboundedness. +#[derive(Debug, Error)] +#[error("Could not construct linear function to map between intervals")] +pub(super) enum LinearMapError { + /// The source interval being mapped out of was unbounded. + #[error("The source interval is unbounded")] + SourceUnbounded, + + /// The target interval being mapped into was unbounded. + #[error("The target interval is unbounded")] + TargetUnbounded, +} + +impl Interval { + /// Create a new [`Interval`] with the specified `start` and `end`. The interval can be unbounded + /// but cannot be empty (so `start` must be less than `end`) and neither endpoint can be NaN; invalid + /// parameters will result in an error. + #[inline] + pub fn new(start: f32, end: f32) -> Result { + if start >= end || start.is_nan() || end.is_nan() { + Err(InvalidIntervalError) + } else { + Ok(Self { start, end }) + } + } + + /// An interval which stretches across the entire real line from negative infinity to infinity. + pub const EVERYWHERE: Self = Self { + start: f32::NEG_INFINITY, + end: f32::INFINITY, + }; + + /// Get the start of this interval. + #[inline] + pub const fn start(self) -> f32 { + self.start + } + + /// Get the end of this interval. + #[inline] + pub const fn end(self) -> f32 { + self.end + } + + /// Create an [`Interval`] by intersecting this interval with another. Returns an error if the + /// intersection would be empty (hence an invalid interval). + pub fn intersect(self, other: Interval) -> Result { + let lower = max_by(self.start, other.start, f32::total_cmp); + let upper = min_by(self.end, other.end, f32::total_cmp); + Self::new(lower, upper) + } + + /// Get the length of this interval. Note that the result may be infinite (`f32::INFINITY`). + #[inline] + pub fn length(self) -> f32 { + self.end - self.start + } + + /// Returns `true` if this interval is bounded — that is, if both its start and end are finite. + /// + /// Equivalently, an interval is bounded if its length is finite. + #[inline] + pub fn is_bounded(self) -> bool { + self.length().is_finite() + } + + /// Returns `true` if this interval has a finite start. + #[inline] + pub fn has_finite_start(self) -> bool { + self.start.is_finite() + } + + /// Returns `true` if this interval has a finite end. + #[inline] + pub fn has_finite_end(self) -> bool { + self.end.is_finite() + } + + /// Returns `true` if `item` is contained in this interval. + #[inline] + pub fn contains(self, item: f32) -> bool { + (self.start..=self.end).contains(&item) + } + + /// Returns `true` if the other interval is contained in this interval. + /// + /// This is non-strict: each interval will contain itself. + #[inline] + pub fn contains_interval(self, other: Self) -> bool { + self.start <= other.start && self.end >= other.end + } + + /// Clamp the given `value` to lie within this interval. + #[inline] + pub fn clamp(self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Get an iterator over equally-spaced points from this interval in increasing order. + /// If `points` is 1, the start of this interval is returned. If `points` is 0, an empty + /// iterator is returned. An error is returned if the interval is unbounded. + #[inline] + pub fn spaced_points( + self, + points: usize, + ) -> Result, SpacedPointsError> { + if !self.is_bounded() { + return Err(SpacedPointsError); + } + if points < 2 { + // If `points` is 1, this is `Some(self.start)` as an iterator, and if `points` is 0, + // then this is `None` as an iterator. This is written this way to avoid having to + // introduce a ternary disjunction of iterators. + let iter = (points == 1).then_some(self.start).into_iter(); + return Ok(Either::Left(iter)); + } + let step = self.length() / (points - 1) as f32; + let iter = (0..points).map(move |x| self.start + x as f32 * step); + Ok(Either::Right(iter)) + } + + /// Get the linear function which maps this interval onto the `other` one. Returns an error if either + /// interval is unbounded. + #[inline] + pub(super) fn linear_map_to(self, other: Self) -> Result f32, LinearMapError> { + if !self.is_bounded() { + return Err(LinearMapError::SourceUnbounded); + } + + if !other.is_bounded() { + return Err(LinearMapError::TargetUnbounded); + } + + let scale = other.length() / self.length(); + Ok(move |x| (x - self.start) * scale + other.start) + } +} + +impl TryFrom> for Interval { + type Error = InvalidIntervalError; + fn try_from(range: RangeInclusive) -> Result { + Interval::new(*range.start(), *range.end()) + } +} + +/// Create an [`Interval`] with a given `start` and `end`. Alias of [`Interval::new`]. +#[inline] +pub fn interval(start: f32, end: f32) -> Result { + Interval::new(start, end) +} + +#[cfg(test)] +mod tests { + use super::*; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + + #[test] + fn make_intervals() { + let ivl = Interval::new(2.0, -1.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(-0.0, 0.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NEG_INFINITY, 15.5); + assert!(ivl.is_ok()); + + let ivl = Interval::new(-2.0, f32::INFINITY); + assert!(ivl.is_ok()); + + let ivl = Interval::new(f32::NEG_INFINITY, f32::INFINITY); + assert!(ivl.is_ok()); + + let ivl = Interval::new(f32::INFINITY, f32::NEG_INFINITY); + assert!(ivl.is_err()); + + let ivl = Interval::new(-1.0, f32::NAN); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NAN, -42.0); + assert!(ivl.is_err()); + + let ivl = Interval::new(f32::NAN, f32::NAN); + assert!(ivl.is_err()); + + let ivl = Interval::new(0.0, 1.0); + assert!(ivl.is_ok()); + } + + #[test] + fn lengths() { + let ivl = interval(-5.0, 10.0).unwrap(); + assert!((ivl.length() - 15.0).abs() <= f32::EPSILON); + + let ivl = interval(5.0, 100.0).unwrap(); + assert!((ivl.length() - 95.0).abs() <= f32::EPSILON); + + let ivl = interval(0.0, f32::INFINITY).unwrap(); + assert_eq!(ivl.length(), f32::INFINITY); + + let ivl = interval(f32::NEG_INFINITY, 0.0).unwrap(); + assert_eq!(ivl.length(), f32::INFINITY); + + let ivl = Interval::EVERYWHERE; + assert_eq!(ivl.length(), f32::INFINITY); + } + + #[test] + fn intersections() { + let ivl1 = interval(-1.0, 1.0).unwrap(); + let ivl2 = interval(0.0, 2.0).unwrap(); + let ivl3 = interval(-3.0, 0.0).unwrap(); + let ivl4 = interval(0.0, f32::INFINITY).unwrap(); + let ivl5 = interval(f32::NEG_INFINITY, 0.0).unwrap(); + let ivl6 = Interval::EVERYWHERE; + + assert!(ivl1 + .intersect(ivl2) + .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1 + .intersect(ivl3) + .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); + assert!(ivl2.intersect(ivl3).is_err()); + assert!(ivl1 + .intersect(ivl4) + .is_ok_and(|ivl| ivl == interval(0.0, 1.0).unwrap())); + assert!(ivl1 + .intersect(ivl5) + .is_ok_and(|ivl| ivl == interval(-1.0, 0.0).unwrap())); + assert!(ivl4.intersect(ivl5).is_err()); + assert_eq!(ivl1.intersect(ivl6).unwrap(), ivl1); + assert_eq!(ivl4.intersect(ivl6).unwrap(), ivl4); + assert_eq!(ivl5.intersect(ivl6).unwrap(), ivl5); + } + + #[test] + fn containment() { + let ivl = interval(0.0, 1.0).unwrap(); + assert!(ivl.contains(0.0)); + assert!(ivl.contains(1.0)); + assert!(ivl.contains(0.5)); + assert!(!ivl.contains(-0.1)); + assert!(!ivl.contains(1.1)); + assert!(!ivl.contains(f32::NAN)); + + let ivl = interval(3.0, f32::INFINITY).unwrap(); + assert!(ivl.contains(3.0)); + assert!(ivl.contains(2.0e5)); + assert!(ivl.contains(3.5e6)); + assert!(!ivl.contains(2.5)); + assert!(!ivl.contains(-1e5)); + assert!(!ivl.contains(f32::NAN)); + } + + #[test] + fn interval_containment() { + let ivl = interval(0.0, 1.0).unwrap(); + assert!(ivl.contains_interval(interval(-0.0, 0.5).unwrap())); + assert!(ivl.contains_interval(interval(0.5, 1.0).unwrap())); + assert!(ivl.contains_interval(interval(0.25, 0.75).unwrap())); + assert!(!ivl.contains_interval(interval(-0.25, 0.5).unwrap())); + assert!(!ivl.contains_interval(interval(0.5, 1.25).unwrap())); + assert!(!ivl.contains_interval(interval(0.25, f32::INFINITY).unwrap())); + assert!(!ivl.contains_interval(interval(f32::NEG_INFINITY, 0.75).unwrap())); + + let big_ivl = interval(0.0, f32::INFINITY).unwrap(); + assert!(big_ivl.contains_interval(interval(0.0, 5.0).unwrap())); + assert!(big_ivl.contains_interval(interval(0.0, f32::INFINITY).unwrap())); + assert!(big_ivl.contains_interval(interval(1.0, 5.0).unwrap())); + assert!(!big_ivl.contains_interval(interval(-1.0, f32::INFINITY).unwrap())); + assert!(!big_ivl.contains_interval(interval(-2.0, 5.0).unwrap())); + } + + #[test] + fn boundedness() { + assert!(!Interval::EVERYWHERE.is_bounded()); + assert!(interval(0.0, 3.5e5).unwrap().is_bounded()); + assert!(!interval(-2.0, f32::INFINITY).unwrap().is_bounded()); + assert!(!interval(f32::NEG_INFINITY, 5.0).unwrap().is_bounded()); + } + + #[test] + fn linear_maps() { + let ivl1 = interval(-3.0, 5.0).unwrap(); + let ivl2 = interval(0.0, 1.0).unwrap(); + let map = ivl1.linear_map_to(ivl2); + assert!(map.is_ok_and(|f| f(-3.0).abs_diff_eq(&0.0, f32::EPSILON) + && f(5.0).abs_diff_eq(&1.0, f32::EPSILON) + && f(1.0).abs_diff_eq(&0.5, f32::EPSILON))); + + let ivl1 = interval(0.0, 1.0).unwrap(); + let ivl2 = Interval::EVERYWHERE; + assert!(ivl1.linear_map_to(ivl2).is_err()); + + let ivl1 = interval(f32::NEG_INFINITY, -4.0).unwrap(); + let ivl2 = interval(0.0, 1.0).unwrap(); + assert!(ivl1.linear_map_to(ivl2).is_err()); + } + + #[test] + fn spaced_points() { + let ivl = interval(0.0, 50.0).unwrap(); + let points_iter: Vec = ivl.spaced_points(1).unwrap().collect(); + assert_abs_diff_eq!(points_iter[0], 0.0); + assert_eq!(points_iter.len(), 1); + let points_iter: Vec = ivl.spaced_points(2).unwrap().collect(); + assert_abs_diff_eq!(points_iter[0], 0.0); + assert_abs_diff_eq!(points_iter[1], 50.0); + let points_iter = ivl.spaced_points(21).unwrap(); + let step = ivl.length() / 20.0; + for (index, point) in points_iter.enumerate() { + let expected = ivl.start() + step * index as f32; + assert_abs_diff_eq!(point, expected); + } + + let ivl = interval(-21.0, 79.0).unwrap(); + let points_iter = ivl.spaced_points(10000).unwrap(); + let step = ivl.length() / 9999.0; + for (index, point) in points_iter.enumerate() { + let expected = ivl.start() + step * index as f32; + assert_abs_diff_eq!(point, expected); + } + + let ivl = interval(-1.0, f32::INFINITY).unwrap(); + let points_iter = ivl.spaced_points(25); + assert!(points_iter.is_err()); + + let ivl = interval(f32::NEG_INFINITY, -25.0).unwrap(); + let points_iter = ivl.spaced_points(9); + assert!(points_iter.is_err()); + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs new file mode 100644 index 00000000000000..03cfd7c74c07f5 --- /dev/null +++ b/crates/bevy_math/src/curve/mod.rs @@ -0,0 +1,1127 @@ +//! The [`Curve`] trait, used to describe curves in a number of different domains. This module also +//! contains the [`Interval`] type, along with a selection of core data structures used to back +//! curves that are interpolated from samples. + +pub mod cores; +pub mod interval; + +pub use interval::{interval, Interval}; +use itertools::Itertools; + +use crate::StableInterpolate; +use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; +use interval::InvalidIntervalError; +use std::{marker::PhantomData, ops::Deref}; +use thiserror::Error; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + +/// A trait for a type that can represent values of type `T` parametrized over a fixed interval. +/// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds +/// of output data can be represented as well. +pub trait Curve { + /// The interval over which this curve is parametrized. + /// + /// This is the range of values of `t` where we can sample the curve and receive valid output. + fn domain(&self) -> Interval; + + /// Sample a point on this curve at the parameter value `t`, extracting the associated value. + /// This is the unchecked version of sampling, which should only be used if the sample time `t` + /// is already known to lie within the curve's domain. + /// + /// Values sampled from outside of a curve's domain are generally considered invalid; data which + /// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation + /// beyond a curve's domain should not be relied upon. + fn sample_unchecked(&self, t: f32) -> T; + + /// Sample a point on this curve at the parameter value `t`, returning `None` if the point is + /// outside of the curve's domain. + fn sample(&self, t: f32) -> Option { + match self.domain().contains(t) { + true => Some(self.sample_unchecked(t)), + false => None, + } + } + + /// Sample a point on this curve at the parameter value `t`, clamping `t` to lie inside the + /// domain of the curve. + fn sample_clamped(&self, t: f32) -> T { + let t = self.domain().clamp(t); + self.sample_unchecked(t) + } + + /// Create a new curve by mapping the values of this curve via a function `f`; i.e., if the + /// sample at time `t` for this curve is `x`, the value at time `t` on the new curve will be + /// `f(x)`. + #[must_use] + fn map(self, f: F) -> MapCurve + where + Self: Sized, + F: Fn(T) -> S, + { + MapCurve { + preimage: self, + f, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] whose parameter space is related to the parameter space of this curve + /// by `f`. For each time `t`, the sample from the new curve at time `t` is the sample from + /// this curve at time `f(t)`. The given `domain` will be the domain of the new curve. The + /// function `f` is expected to take `domain` into `self.domain()`. + /// + /// Note that this is the opposite of what one might expect intuitively; for example, if this + /// curve has a parameter domain of `[0, 1]`, then stretching the parameter domain to + /// `[0, 2]` would be performed as follows, dividing by what might be perceived as the scaling + /// factor rather than multiplying: + /// ``` + /// # use bevy_math::curve::*; + /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); + /// ``` + /// This kind of linear remapping is provided by the convenience method + /// [`Curve::reparametrize_linear`], which requires only the desired domain for the new curve. + /// + /// # Examples + /// ``` + /// // Reverse a curve: + /// # use bevy_math::curve::*; + /// # use bevy_math::vec2; + /// let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let domain = my_curve.domain(); + /// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - t); + /// + /// // Take a segment of a curve: + /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); + /// + /// // Reparametrize by an easing curve: + /// # let my_curve = constant_curve(interval(0.0, 1.0).unwrap(), 1.0); + /// # let easing_curve = constant_curve(interval(0.0, 1.0).unwrap(), vec2(1.0, 1.0)); + /// let domain = my_curve.domain(); + /// let eased_curve = my_curve.reparametrize(domain, |t| easing_curve.sample_unchecked(t).y); + /// ``` + #[must_use] + fn reparametrize(self, domain: Interval, f: F) -> ReparamCurve + where + Self: Sized, + F: Fn(f32) -> f32, + { + ReparamCurve { + domain, + base: self, + f, + _phantom: PhantomData, + } + } + + /// Linearly reparametrize this [`Curve`], producing a new curve whose domain is the given + /// `domain` instead of the current one. This operation is only valid for curves with bounded + /// domains; if either this curve's domain or the given `domain` is unbounded, an error is + /// returned. + fn reparametrize_linear( + self, + domain: Interval, + ) -> Result, LinearReparamError> + where + Self: Sized, + { + if !self.domain().is_bounded() { + return Err(LinearReparamError::SourceCurveUnbounded); + } + + if !domain.is_bounded() { + return Err(LinearReparamError::TargetIntervalUnbounded); + } + + Ok(LinearReparamCurve { + base: self, + new_domain: domain, + _phantom: PhantomData, + }) + } + + /// Reparametrize this [`Curve`] by sampling from another curve. + /// + /// The resulting curve samples at time `t` by first sampling `other` at time `t`, which produces + /// another sample time `s` which is then used to sample this curve. The domain of the resulting + /// curve is the domain of `other`. + #[must_use] + fn reparametrize_by_curve(self, other: C) -> CurveReparamCurve + where + Self: Sized, + C: Curve, + { + CurveReparamCurve { + base: self, + reparam_curve: other, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] which is the graph of this one; that is, its output echoes the sample + /// time as part of a tuple. + /// + /// For example, if this curve outputs `x` at time `t`, then the produced curve will produce + /// `(t, x)` at time `t`. In particular, if this curve is a `Curve`, the output of this method + /// is a `Curve<(f32, T)>`. + #[must_use] + fn graph(self) -> GraphCurve + where + Self: Sized, + { + GraphCurve { + base: self, + _phantom: PhantomData, + } + } + + /// Create a new [`Curve`] by zipping this curve together with another. + /// + /// The sample at time `t` in the new curve is `(x, y)`, where `x` is the sample of `self` at + /// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the + /// intersection of the domains of its constituents. If the domain intersection would be empty, + /// an error is returned. + fn zip(self, other: C) -> Result, InvalidIntervalError> + where + Self: Sized, + C: Curve + Sized, + { + let domain = self.domain().intersect(other.domain())?; + Ok(ProductCurve { + domain, + first: self, + second: other, + _phantom: PhantomData, + }) + } + + /// Create a new [`Curve`] by composing this curve end-to-end with another, producing another curve + /// with outputs of the same type. The domain of the other curve is translated so that its start + /// coincides with where this curve ends. A [`ChainError`] is returned if this curve's domain + /// doesn't have a finite end or if `other`'s domain doesn't have a finite start. + fn chain(self, other: C) -> Result, ChainError> + where + Self: Sized, + C: Curve, + { + if !self.domain().has_finite_end() { + return Err(ChainError::FirstEndInfinite); + } + if !other.domain().has_finite_start() { + return Err(ChainError::SecondStartInfinite); + } + Ok(ChainCurve { + first: self, + second: other, + _phantom: PhantomData, + }) + } + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples. + /// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1, + /// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at + /// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded + /// domain, then a [`ResamplingError`] is returned. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + /// + /// # Example + /// ``` + /// # use bevy_math::*; + /// # use bevy_math::curve::*; + /// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t)); + /// // A curve which only stores three data points and uses `nlerp` to interpolate them: + /// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t)); + /// ``` + fn resample( + &self, + segments: usize, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + let samples = self.samples(segments + 1)?.collect_vec(); + Ok(SampleCurve { + core: EvenCore { + domain: self.domain(), + samples, + }, + interpolation, + }) + } + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally + /// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples. + /// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1, + /// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at + /// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded + /// domain, then a [`ResamplingError`] is returned. + /// + /// [automatic interpolation]: crate::common_traits::StableInterpolate + fn resample_auto(&self, segments: usize) -> Result, ResamplingError> + where + Self: Sized, + T: StableInterpolate, + { + let samples = self.samples(segments + 1)?.collect_vec(); + Ok(SampleAutoCurve { + core: EvenCore { + domain: self.domain(), + samples, + }, + }) + } + + /// Extract an iterator over evenly-spaced samples from this curve. If `samples` is less than 2 + /// or if this curve has unbounded domain, then an error is returned instead. + fn samples(&self, samples: usize) -> Result, ResamplingError> + where + Self: Sized, + { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_bounded() { + return Err(ResamplingError::UnboundedDomain); + } + + // Unwrap on `spaced_points` always succeeds because its error conditions are handled + // above. + Ok(self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample_unchecked(t))) + } + + /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples + /// taken at a given set of times. The given `interpolation` is used to interpolate adjacent + /// samples, and the `sample_times` are expected to contain at least two valid times within the + /// curve's domain interval. + /// + /// Redundant sample times, non-finite sample times, and sample times outside of the domain + /// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is + /// returned. + /// + /// The domain of the produced curve stretches between the first and last sample times of the + /// iterator. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + fn resample_uneven( + &self, + sample_times: impl IntoIterator, + interpolation: I, + ) -> Result, ResamplingError> + where + Self: Sized, + I: Fn(&T, &T, f32) -> T, + { + let domain = self.domain(); + let mut times = sample_times + .into_iter() + .filter(|t| t.is_finite() && domain.contains(*t)) + .collect_vec(); + times.sort_by(f32::total_cmp); + times.dedup(); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect(); + Ok(UnevenSampleCurve { + core: UnevenCore { times, samples }, + interpolation, + }) + } + + /// Resample this [`Curve`] to produce a new one that is defined by [automatic interpolation] over + /// samples taken at the given set of times. The given `sample_times` are expected to contain at least + /// two valid times within the curve's domain interval. + /// + /// Redundant sample times, non-finite sample times, and sample times outside of the domain + /// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is + /// returned. + /// + /// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last + /// sample times of the iterator. + /// + /// [automatic interpolation]: crate::common_traits::StableInterpolate + fn resample_uneven_auto( + &self, + sample_times: impl IntoIterator, + ) -> Result, ResamplingError> + where + Self: Sized, + T: StableInterpolate, + { + let domain = self.domain(); + let mut times = sample_times + .into_iter() + .filter(|t| t.is_finite() && domain.contains(*t)) + .collect_vec(); + times.sort_by(f32::total_cmp); + times.dedup(); + if times.len() < 2 { + return Err(ResamplingError::NotEnoughSamples(times.len())); + } + let samples = times.iter().map(|t| self.sample_unchecked(*t)).collect(); + Ok(UnevenSampleAutoCurve { + core: UnevenCore { times, samples }, + }) + } + + /// Borrow this curve rather than taking ownership of it. This is essentially an alias for a + /// prefix `&`; the point is that intermediate operations can be performed while retaining + /// access to the original curve. + /// + /// # Example + /// ``` + /// # use bevy_math::curve::*; + /// let my_curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * t + 1.0); + /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes + /// // ownership of its input. + /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); + /// // Do something else with `my_curve` since we retained ownership: + /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); + /// ``` + fn by_ref(&self) -> &Self + where + Self: Sized, + { + self + } + + /// Flip this curve so that its tuple output is arranged the other way. + #[must_use] + fn flip(self) -> impl Curve<(V, U)> + where + Self: Sized + Curve<(U, V)>, + { + self.map(|(u, v)| (v, u)) + } +} + +impl Curve for D +where + C: Curve + ?Sized, + D: Deref, +{ + fn domain(&self) -> Interval { + >::domain(self) + } + + fn sample_unchecked(&self, t: f32) -> T { + >::sample_unchecked(self, t) + } +} + +/// An error indicating that a linear reparametrization couldn't be performed because of +/// malformed inputs. +#[derive(Debug, Error)] +#[error("Could not build a linear function to reparametrize this curve")] +pub enum LinearReparamError { + /// The source curve that was to be reparametrized had unbounded domain. + #[error("This curve has unbounded domain")] + SourceCurveUnbounded, + + /// The target interval for reparametrization was unbounded. + #[error("The target interval for reparametrization is unbounded")] + TargetIntervalUnbounded, +} + +/// An error indicating that an end-to-end composition couldn't be performed because of +/// malformed inputs. +#[derive(Debug, Error)] +#[error("Could not compose these curves together")] +pub enum ChainError { + /// The right endpoint of the first curve was infinite. + #[error("The first curve's domain has an infinite end")] + FirstEndInfinite, + + /// The left endpoint of the second curve was infinite. + #[error("The second curve's domain has an infinite start")] + SecondStartInfinite, +} + +/// An error indicating that a resampling operation could not be performed because of +/// malformed inputs. +#[derive(Debug, Error)] +#[error("Could not resample from this curve because of bad inputs")] +pub enum ResamplingError { + /// This resampling operation was not provided with enough samples to have well-formed output. + #[error("Not enough unique samples to construct resampled curve")] + NotEnoughSamples(usize), + + /// This resampling operation failed because of an unbounded interval. + #[error("Could not resample because this curve has unbounded domain")] + UnboundedDomain, +} + +/// A curve with a constant value over its domain. +/// +/// This is a curve that holds an inner value and always produces a clone of that value when sampled. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ConstantCurve { + domain: Interval, + value: T, +} + +impl ConstantCurve +where + T: Clone, +{ + /// Create a constant curve, which has the given `domain` and always produces the given `value` + /// when sampled. + pub fn new(domain: Interval, value: T) -> Self { + Self { domain, value } + } +} + +impl Curve for ConstantCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, _t: f32) -> T { + self.value.clone() + } +} + +/// A curve defined by a function together with a fixed domain. +/// +/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces +/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct FunctionCurve { + domain: Interval, + f: F, + _phantom: PhantomData, +} + +impl FunctionCurve +where + F: Fn(f32) -> T, +{ + /// Create a new curve with the given `domain` from the given `function`. When sampled, the + /// `function` is evaluated at the sample time to compute the output. + pub fn new(domain: Interval, function: F) -> Self { + FunctionCurve { + domain, + f: function, + _phantom: PhantomData, + } + } +} + +impl Curve for FunctionCurve +where + F: Fn(f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(t) + } +} + +/// A curve whose samples are defined by mapping samples from another curve through a +/// given function. Curves of this type are produced by [`Curve::map`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct MapCurve { + preimage: C, + f: F, + _phantom: PhantomData<(S, T)>, +} + +impl Curve for MapCurve +where + C: Curve, + F: Fn(S) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.preimage.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(self.preimage.sample_unchecked(t)) + } +} + +/// A curve whose sample space is mapped onto that of some base curve's before sampling. +/// Curves of this type are produced by [`Curve::reparametrize`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ReparamCurve { + domain: Interval, + base: C, + f: F, + _phantom: PhantomData, +} + +impl Curve for ReparamCurve +where + C: Curve, + F: Fn(f32) -> f32, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.base.sample_unchecked((self.f)(t)) + } +} + +/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling). +/// Curves of this type are produced by [`Curve::reparametrize_linear`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct LinearReparamCurve { + /// Invariants: The domain of this curve must always be bounded. + base: C, + /// Invariants: This interval must always be bounded. + new_domain: Interval, + _phantom: PhantomData, +} + +impl Curve for LinearReparamCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.new_domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + // The invariants imply this unwrap always succeeds. + let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); + self.base.sample_unchecked(f(t)) + } +} + +/// A curve that has been reparametrized by another curve, using that curve to transform the +/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct CurveReparamCurve { + base: C, + reparam_curve: D, + _phantom: PhantomData, +} + +impl Curve for CurveReparamCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.reparam_curve.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + let sample_time = self.reparam_curve.sample_unchecked(t); + self.base.sample_unchecked(sample_time) + } +} + +/// A curve that is the graph of another curve over its parameter space. Curves of this type are +/// produced by [`Curve::graph`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct GraphCurve { + base: C, + _phantom: PhantomData, +} + +impl Curve<(f32, T)> for GraphCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.base.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> (f32, T) { + (t, self.base.sample_unchecked(t)) + } +} + +/// A curve that combines the output data from two constituent curves into a tuple output. Curves +/// of this type are produced by [`Curve::zip`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ProductCurve { + domain: Interval, + first: C, + second: D, + _phantom: PhantomData<(S, T)>, +} + +impl Curve<(S, T)> for ProductCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> (S, T) { + ( + self.first.sample_unchecked(t), + self.second.sample_unchecked(t), + ) + } +} + +/// The curve that results from chaining one curve with another. The second curve is +/// effectively reparametrized so that its start is at the end of the first. +/// +/// For this to be well-formed, the first curve's domain must be right-finite and the second's +/// must be left-finite. +/// +/// Curves of this type are produced by [`Curve::chain`]. +pub struct ChainCurve { + first: C, + second: D, + _phantom: PhantomData, +} + +impl Curve for ChainCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + // This unwrap always succeeds because `first` has a valid Interval as its domain and the + // length of `second` cannot be NAN. It's still fine if it's infinity. + Interval::new( + self.first.domain().start(), + self.first.domain().end() + self.second.domain().length(), + ) + .unwrap() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + if t > self.first.domain().end() { + self.second.sample_unchecked( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ) + } else { + self.first.sample_unchecked(t) + } + } +} + +/// A curve that is defined by explicit neighbor interpolation over a set of samples. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct SampleCurve { + core: EvenCore, + interpolation: I, +} + +impl Curve for SampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, &self.interpolation) + } +} + +impl SampleCurve { + /// Create a new [`SampleCurve`] using the specified `interpolation` to interpolate between + /// the given `samples`. An error is returned if there are not at least 2 samples or if the + /// given `domain` is unbounded. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + pub fn new( + domain: Interval, + samples: impl IntoIterator, + interpolation: I, + ) -> Result + where + I: Fn(&T, &T, f32) -> T, + { + Ok(Self { + core: EvenCore::new(domain, samples)?, + interpolation, + }) + } +} + +/// A curve that is defined by neighbor interpolation over a set of samples. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct SampleAutoCurve { + core: EvenCore, +} + +impl Curve for SampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl SampleAutoCurve { + /// Create a new [`SampleCurve`] using type-inferred interpolation to interpolate between + /// the given `samples`. An error is returned if there are not at least 2 samples or if the + /// given `domain` is unbounded. + pub fn new( + domain: Interval, + samples: impl IntoIterator, + ) -> Result { + Ok(Self { + core: EvenCore::new(domain, samples)?, + }) + } +} + +/// A curve that is defined by interpolation over unevenly spaced samples with explicit +/// interpolation. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct UnevenSampleCurve { + core: UnevenCore, + interpolation: I, +} + +impl Curve for UnevenSampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, &self.interpolation) + } +} + +impl UnevenSampleCurve { + /// Create a new [`UnevenSampleCurve`] using the provided `interpolation` to interpolate + /// between adjacent `timed_samples`. The given samples are filtered to finite times and + /// sorted internally; if there are not at least 2 valid timed samples, an error will be + /// returned. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + pub fn new( + timed_samples: impl IntoIterator, + interpolation: I, + ) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + interpolation, + }) + } + + /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. + /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// but the function inputs to each are inverses of one another. + /// + /// The samples are re-sorted by time after mapping and deduplicated by output time, so + /// the function `f` should generally be injective over the sample times of the curve. + pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + Self { + core: self.core.map_sample_times(f), + interpolation: self.interpolation, + } + } +} + +/// A curve that is defined by interpolation over unevenly spaced samples. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct UnevenSampleAutoCurve { + core: UnevenCore, +} + +impl Curve for UnevenSampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl UnevenSampleAutoCurve { + /// Create a new [`UnevenSampleAutoCurve`] from a given set of timed samples, interpolated + /// using the The samples are filtered to finite times and + /// sorted internally; if there are not at least 2 valid timed samples, an error will be + /// returned. + pub fn new(timed_samples: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + }) + } + + /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. + /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// but the function inputs to each are inverses of one another. + /// + /// The samples are re-sorted by time after mapping and deduplicated by output time, so + /// the function `f` should generally be injective over the sample times of the curve. + pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { + Self { + core: self.core.map_sample_times(f), + } + } +} + +/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. +pub fn constant_curve(domain: Interval, value: T) -> ConstantCurve { + ConstantCurve { domain, value } +} + +/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by +/// evaluating the function. +pub fn function_curve(domain: Interval, f: F) -> FunctionCurve +where + F: Fn(f32) -> T, +{ + FunctionCurve { + domain, + f, + _phantom: PhantomData, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ops, Quat}; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + use std::f32::consts::TAU; + + #[test] + fn constant_curves() { + let curve = constant_curve(Interval::EVERYWHERE, 5.0); + assert!(curve.sample_unchecked(-35.0) == 5.0); + + let curve = constant_curve(interval(0.0, 1.0).unwrap(), true); + assert!(curve.sample_unchecked(2.0)); + assert!(curve.sample(2.0).is_none()); + } + + #[test] + fn function_curves() { + let curve = function_curve(Interval::EVERYWHERE, |t| t * t); + assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON)); + assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); + + let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::log2); + assert_eq!(curve.sample_unchecked(3.5), ops::log2(3.5)); + assert!(curve.sample_unchecked(-1.0).is_nan()); + assert!(curve.sample(-1.0).is_none()); + } + + #[test] + fn mapping() { + let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); + let mapped_curve = curve.map(|x| x / 7.0); + assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0); + assert_eq!( + mapped_curve.sample_unchecked(-1.0), + (-1.0 * 3.0 + 1.0) / 7.0 + ); + assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE); + + let curve = function_curve(interval(0.0, 1.0).unwrap(), |t| t * TAU); + let mapped_curve = curve.map(Quat::from_rotation_z); + assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY); + assert!(mapped_curve.sample_unchecked(1.0).is_near_identity()); + assert_eq!(mapped_curve.domain(), interval(0.0, 1.0).unwrap()); + } + + #[test] + fn reparametrization() { + let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, f32::INFINITY).unwrap(), ops::exp2); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(3.5), 3.5); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(100.0), 100.0); + assert_eq!( + reparametrized_curve.domain(), + interval(0.0, f32::INFINITY).unwrap() + ); + + let reparametrized_curve = curve + .by_ref() + .reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(0.0), 0.0); + assert_abs_diff_eq!(reparametrized_curve.sample_unchecked(1.0), 1.0); + assert_eq!(reparametrized_curve.domain(), interval(0.0, 1.0).unwrap()); + } + + #[test] + fn multiple_maps() { + // Make sure these actually happen in the right order. + let curve = function_curve(interval(0.0, 1.0).unwrap(), ops::exp2); + let first_mapped = curve.map(ops::log2); + let second_mapped = first_mapped.map(|x| x * -2.0); + assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0); + assert_abs_diff_eq!(second_mapped.sample_unchecked(0.5), -1.0); + assert_abs_diff_eq!(second_mapped.sample_unchecked(1.0), -2.0); + } + + #[test] + fn multiple_reparams() { + // Make sure these happen in the right order too. + let curve = function_curve(interval(0.0, 1.0).unwrap(), ops::exp2); + let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), ops::log2); + let second_reparam = first_reparam.reparametrize(interval(0.0, 1.0).unwrap(), |t| t + 1.0); + assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0); + assert_abs_diff_eq!(second_reparam.sample_unchecked(0.5), 1.5); + assert_abs_diff_eq!(second_reparam.sample_unchecked(1.0), 2.0); + } + + #[test] + fn resampling() { + let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2); + + // Need at least one segment to sample. + let nice_try = curve.by_ref().resample_auto(0); + assert!(nice_try.is_err()); + + // The values of a resampled curve should be very close at the sample points. + // Because of denominators, it's not literally equal. + // (This is a tradeoff against O(1) sampling.) + let resampled_curve = curve.by_ref().resample_auto(100).unwrap(); + for test_pt in curve.domain().spaced_points(101).unwrap() { + let expected = curve.sample_unchecked(test_pt); + assert_abs_diff_eq!( + resampled_curve.sample_unchecked(test_pt), + expected, + epsilon = 1e-6 + ); + } + + // Another example. + let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); + let resampled_curve = curve.by_ref().resample_auto(1000).unwrap(); + for test_pt in curve.domain().spaced_points(1001).unwrap() { + let expected = curve.sample_unchecked(test_pt); + assert_abs_diff_eq!( + resampled_curve.sample_unchecked(test_pt), + expected, + epsilon = 1e-6 + ); + } + } + + #[test] + fn uneven_resampling() { + let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::exp); + + // Need at least two points to resample. + let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]); + assert!(nice_try.is_err()); + + // Uneven sampling should produce literal equality at the sample points. + // (This is part of what you get in exchange for O(log(n)) sampling.) + let sample_points = (0..100).map(|idx| idx as f32 * 0.1); + let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); + for idx in 0..100 { + let test_pt = idx as f32 * 0.1; + let expected = curve.sample_unchecked(test_pt); + assert_eq!(resampled_curve.sample_unchecked(test_pt), expected); + } + assert_abs_diff_eq!(resampled_curve.domain().start(), 0.0); + assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6); + + // Another example. + let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); + let sample_points = (0..10).map(|idx| ops::exp2(idx as f32)); + let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); + for idx in 0..10 { + let test_pt = ops::exp2(idx as f32); + let expected = curve.sample_unchecked(test_pt); + assert_eq!(resampled_curve.sample_unchecked(test_pt), expected); + } + assert_abs_diff_eq!(resampled_curve.domain().start(), 1.0); + assert_abs_diff_eq!(resampled_curve.domain().end(), 512.0); + } +} diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index df061195a73083..8f4cf7d9eababf 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -169,6 +169,15 @@ impl Dir2 { Self::new(Vec2::new(x, y)) } + /// Create a direction from its `x` and `y` components, assuming the resulting vector is normalized. + /// + /// # Warning + /// + /// The vector produced from `x` and `y` must be normalized, i.e its length must be `1.0`. + pub fn from_xy_unchecked(x: f32, y: f32) -> Self { + Self::new_unchecked(Vec2::new(x, y)) + } + /// Returns the inner [`Vec2`] pub const fn as_vec2(&self) -> Vec2 { self.0 @@ -199,7 +208,7 @@ impl Dir2 { /// ``` #[inline] pub fn slerp(self, rhs: Self, s: f32) -> Self { - let angle = self.angle_between(rhs.0); + let angle = self.angle_to(rhs.0); Rot2::radians(angle * s) * self } @@ -229,7 +238,7 @@ impl Dir2 { self.rotation_from_x().inverse() } - /// Get the rotation that rotates this direction to the Y-axis. + /// Get the rotation that rotates the Y-axis to this direction. #[inline] pub fn rotation_from_y(self) -> Rot2 { // `x <- y`, `y <- -x` correspond to rotating clockwise by pi/2; @@ -238,11 +247,21 @@ impl Dir2 { Rot2::from_sin_cos(-self.0.x, self.0.y) } - /// Get the rotation that rotates the Y-axis to this direction. + /// Get the rotation that rotates this direction to the Y-axis. #[inline] pub fn rotation_to_y(self) -> Rot2 { self.rotation_from_y().inverse() } + + /// Returns `self` after an approximate normalization, assuming the value is already nearly normalized. + /// Useful for preventing numerical error accumulation. + /// See [`Dir3::fast_renormalize`] for an example of when such error accumulation might occur. + #[inline] + pub fn fast_renormalize(self) -> Self { + let length_squared = self.0.length_squared(); + // Based on a Taylor approximation of the inverse square root, see [`Dir3::fast_renormalize`] for more details. + Self(self * (0.5 * (3.0 - length_squared))) + } } impl TryFrom for Dir2 { @@ -409,6 +428,15 @@ impl Dir3 { Self::new(Vec3::new(x, y, z)) } + /// Create a direction from its `x`, `y`, and `z` components, assuming the resulting vector is normalized. + /// + /// # Warning + /// + /// The vector produced from `x`, `y`, and `z` must be normalized, i.e its length must be `1.0`. + pub fn from_xyz_unchecked(x: f32, y: f32, z: f32) -> Self { + Self::new_unchecked(Vec3::new(x, y, z)) + } + /// Returns the inner [`Vec3`] pub const fn as_vec3(&self) -> Vec3 { self.0 @@ -446,6 +474,56 @@ impl Dir3 { let quat = Quat::IDENTITY.slerp(Quat::from_rotation_arc(self.0, rhs.0), s); Dir3(quat.mul_vec3(self.0)) } + + /// Returns `self` after an approximate normalization, assuming the value is already nearly normalized. + /// Useful for preventing numerical error accumulation. + /// + /// # Example + /// The following seemingly benign code would start accumulating errors over time, + /// leading to `dir` eventually not being normalized anymore. + /// ``` + /// # use bevy_math::prelude::*; + /// # let N: usize = 200; + /// let mut dir = Dir3::X; + /// let quaternion = Quat::from_euler(EulerRot::XYZ, 1.0, 2.0, 3.0); + /// for i in 0..N { + /// dir = quaternion * dir; + /// } + /// ``` + /// Instead, do the following. + /// ``` + /// # use bevy_math::prelude::*; + /// # let N: usize = 200; + /// let mut dir = Dir3::X; + /// let quaternion = Quat::from_euler(EulerRot::XYZ, 1.0, 2.0, 3.0); + /// for i in 0..N { + /// dir = quaternion * dir; + /// dir = dir.fast_renormalize(); + /// } + /// ``` + #[inline] + pub fn fast_renormalize(self) -> Self { + // We numerically approximate the inverse square root by a Taylor series around 1 + // As we expect the error (x := length_squared - 1) to be small + // inverse_sqrt(length_squared) = (1 + x)^(-1/2) = 1 - 1/2 x + O(x²) + // inverse_sqrt(length_squared) ≈ 1 - 1/2 (length_squared - 1) = 1/2 (3 - length_squared) + + // Iterative calls to this method quickly converge to a normalized value, + // so long as the denormalization is not large ~ O(1/10). + // One iteration can be described as: + // l_sq <- l_sq * (1 - 1/2 (l_sq - 1))²; + // Rewriting in terms of the error x: + // 1 + x <- (1 + x) * (1 - 1/2 x)² + // 1 + x <- (1 + x) * (1 - x + 1/4 x²) + // 1 + x <- 1 - x + 1/4 x² + x - x² + 1/4 x³ + // x <- -1/4 x² (3 - x) + // If the error is small, say in a range of (-1/2, 1/2), then: + // |-1/4 x² (3 - x)| <= (3/4 + 1/4 * |x|) * x² <= (3/4 + 1/4 * 1/2) * x² < x² < 1/2 x + // Therefore the sequence of iterates converges to 0 error as a second order method. + + let length_squared = self.0.length_squared(); + Self(self * (0.5 * (3.0 - length_squared))) + } } impl TryFrom for Dir3 { @@ -615,6 +693,15 @@ impl Dir3A { Self::new(Vec3A::new(x, y, z)) } + /// Create a direction from its `x`, `y`, and `z` components, assuming the resulting vector is normalized. + /// + /// # Warning + /// + /// The vector produced from `x`, `y`, and `z` must be normalized, i.e its length must be `1.0`. + pub fn from_xyz_unchecked(x: f32, y: f32, z: f32) -> Self { + Self::new_unchecked(Vec3A::new(x, y, z)) + } + /// Returns the inner [`Vec3A`] pub const fn as_vec3a(&self) -> Vec3A { self.0 @@ -655,6 +742,17 @@ impl Dir3A { ); Dir3A(quat.mul_vec3a(self.0)) } + + /// Returns `self` after an approximate normalization, assuming the value is already nearly normalized. + /// Useful for preventing numerical error accumulation. + /// + /// See [`Dir3::fast_renormalize`] for an example of when such error accumulation might occur. + #[inline] + pub fn fast_renormalize(self) -> Self { + let length_squared = self.0.length_squared(); + // Based on a Taylor approximation of the inverse square root, see [`Dir3::fast_renormalize`] for more details. + Self(self * (0.5 * (3.0 - length_squared))) + } } impl From for Dir3A { @@ -762,6 +860,8 @@ impl approx::UlpsEq for Dir3A { #[cfg(test)] mod tests { + use crate::ops; + use super::*; use approx::assert_relative_eq; @@ -815,6 +915,31 @@ mod tests { assert_relative_eq!(Dir2::NORTH_WEST.rotation_from_y(), Rot2::FRAC_PI_4); } + #[test] + fn dir2_renorm() { + // Evil denormalized Rot2 + let (sin, cos) = ops::sin_cos(1.0_f32); + let rot2 = Rot2::from_sin_cos(sin * (1.0 + 1e-5), cos * (1.0 + 1e-5)); + let mut dir_a = Dir2::X; + let mut dir_b = Dir2::X; + + // We test that renormalizing an already normalized dir doesn't do anything + assert_relative_eq!(dir_b, dir_b.fast_renormalize(), epsilon = 0.000001); + + for _ in 0..50 { + dir_a = rot2 * dir_a; + dir_b = rot2 * dir_b; + dir_b = dir_b.fast_renormalize(); + } + + // `dir_a` should've gotten denormalized, meanwhile `dir_b` should stay normalized. + assert!( + !dir_a.is_normalized(), + "Dernormalization doesn't work, test is faulty" + ); + assert!(dir_b.is_normalized(), "Renormalisation did not work."); + } + #[test] fn dir3_creation() { assert_eq!(Dir3::new(Vec3::X * 12.5), Ok(Dir3::X)); @@ -862,6 +987,30 @@ mod tests { ); } + #[test] + fn dir3_renorm() { + // Evil denormalized quaternion + let rot3 = Quat::from_euler(glam::EulerRot::XYZ, 1.0, 2.0, 3.0) * (1.0 + 1e-5); + let mut dir_a = Dir3::X; + let mut dir_b = Dir3::X; + + // We test that renormalizing an already normalized dir doesn't do anything + assert_relative_eq!(dir_b, dir_b.fast_renormalize(), epsilon = 0.000001); + + for _ in 0..50 { + dir_a = rot3 * dir_a; + dir_b = rot3 * dir_b; + dir_b = dir_b.fast_renormalize(); + } + + // `dir_a` should've gotten denormalized, meanwhile `dir_b` should stay normalized. + assert!( + !dir_a.is_normalized(), + "Dernormalization doesn't work, test is faulty" + ); + assert!(dir_b.is_normalized(), "Renormalisation did not work."); + } + #[test] fn dir3a_creation() { assert_eq!(Dir3A::new(Vec3A::X * 12.5), Ok(Dir3A::X)); @@ -908,4 +1057,28 @@ mod tests { Dir3A::from_xyz(0.0, 0.75f32.sqrt(), 0.5).unwrap() ); } + + #[test] + fn dir3a_renorm() { + // Evil denormalized quaternion + let rot3 = Quat::from_euler(glam::EulerRot::XYZ, 1.0, 2.0, 3.0) * (1.0 + 1e-5); + let mut dir_a = Dir3A::X; + let mut dir_b = Dir3A::X; + + // We test that renormalizing an already normalized dir doesn't do anything + assert_relative_eq!(dir_b, dir_b.fast_renormalize(), epsilon = 0.000001); + + for _ in 0..50 { + dir_a = rot3 * dir_a; + dir_b = rot3 * dir_b; + dir_b = dir_b.fast_renormalize(); + } + + // `dir_a` should've gotten denormalized, meanwhile `dir_b` should stay normalized. + assert!( + !dir_a.is_normalized(), + "Dernormalization doesn't work, test is faulty" + ); + assert!(dir_b.is_normalized(), "Renormalisation did not work."); + } } diff --git a/crates/bevy_math/src/isometry.rs b/crates/bevy_math/src/isometry.rs new file mode 100644 index 00000000000000..a6505eb846c5d1 --- /dev/null +++ b/crates/bevy_math/src/isometry.rs @@ -0,0 +1,652 @@ +//! Isometry types for expressing rigid motions in two and three dimensions. + +use crate::{Affine2, Affine3, Affine3A, Dir2, Dir3, Mat3, Mat3A, Quat, Rot2, Vec2, Vec3, Vec3A}; +use std::ops::Mul; + +#[cfg(feature = "approx")] +use approx::{AbsDiffEq, RelativeEq, UlpsEq}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +#[cfg(all(feature = "bevy_reflect", feature = "serialize"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// An isometry in two dimensions, representing a rotation followed by a translation. +/// This can often be useful for expressing relative positions and transformations from one position to another. +/// +/// In particular, this type represents a distance-preserving transformation known as a *rigid motion* or a *direct motion*, +/// and belongs to the special [Euclidean group] SE(2). This includes translation and rotation, but excludes reflection. +/// +/// For the three-dimensional version, see [`Isometry3d`]. +/// +/// [Euclidean group]: https://en.wikipedia.org/wiki/Euclidean_group +/// +/// # Example +/// +/// Isometries can be created from a given translation and rotation: +/// +/// ``` +/// # use bevy_math::{Isometry2d, Rot2, Vec2}; +/// # +/// let iso = Isometry2d::new(Vec2::new(2.0, 1.0), Rot2::degrees(90.0)); +/// ``` +/// +/// Or from separate parts: +/// +/// ``` +/// # use bevy_math::{Isometry2d, Rot2, Vec2}; +/// # +/// let iso1 = Isometry2d::from_translation(Vec2::new(2.0, 1.0)); +/// let iso2 = Isometry2d::from_rotation(Rot2::degrees(90.0)); +/// ``` +/// +/// The isometries can be used to transform points: +/// +/// ``` +/// # use approx::assert_abs_diff_eq; +/// # use bevy_math::{Isometry2d, Rot2, Vec2}; +/// # +/// let iso = Isometry2d::new(Vec2::new(2.0, 1.0), Rot2::degrees(90.0)); +/// let point = Vec2::new(4.0, 4.0); +/// +/// // These are equivalent +/// let result = iso.transform_point(point); +/// let result = iso * point; +/// +/// assert_eq!(result, Vec2::new(-2.0, 5.0)); +/// ``` +/// +/// Isometries can also be composed together: +/// +/// ``` +/// # use bevy_math::{Isometry2d, Rot2, Vec2}; +/// # +/// # let iso = Isometry2d::new(Vec2::new(2.0, 1.0), Rot2::degrees(90.0)); +/// # let iso1 = Isometry2d::from_translation(Vec2::new(2.0, 1.0)); +/// # let iso2 = Isometry2d::from_rotation(Rot2::degrees(90.0)); +/// # +/// assert_eq!(iso1 * iso2, iso); +/// ``` +/// +/// One common operation is to compute an isometry representing the relative positions of two objects +/// for things like intersection tests. This can be done with an inverse transformation: +/// +/// ``` +/// # use bevy_math::{Isometry2d, Rot2, Vec2}; +/// # +/// let circle_iso = Isometry2d::from_translation(Vec2::new(2.0, 1.0)); +/// let rectangle_iso = Isometry2d::from_rotation(Rot2::degrees(90.0)); +/// +/// // Compute the relative position and orientation between the two shapes +/// let relative_iso = circle_iso.inverse() * rectangle_iso; +/// +/// // Or alternatively, to skip an extra rotation operation: +/// let relative_iso = circle_iso.inverse_mul(rectangle_iso); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Isometry2d { + /// The rotational part of a two-dimensional isometry. + pub rotation: Rot2, + /// The translational part of a two-dimensional isometry. + pub translation: Vec2, +} + +impl Isometry2d { + /// The identity isometry which represents the rigid motion of not doing anything. + pub const IDENTITY: Self = Isometry2d { + rotation: Rot2::IDENTITY, + translation: Vec2::ZERO, + }; + + /// Create a two-dimensional isometry from a rotation and a translation. + #[inline] + pub fn new(translation: Vec2, rotation: Rot2) -> Self { + Isometry2d { + rotation, + translation, + } + } + + /// Create a two-dimensional isometry from a rotation. + #[inline] + pub fn from_rotation(rotation: Rot2) -> Self { + Isometry2d { + rotation, + translation: Vec2::ZERO, + } + } + + /// Create a two-dimensional isometry from a translation. + #[inline] + pub fn from_translation(translation: Vec2) -> Self { + Isometry2d { + rotation: Rot2::IDENTITY, + translation, + } + } + + /// Create a two-dimensional isometry from a translation with the given `x` and `y` components. + #[inline] + pub fn from_xy(x: f32, y: f32) -> Self { + Isometry2d { + rotation: Rot2::IDENTITY, + translation: Vec2::new(x, y), + } + } + + /// The inverse isometry that undoes this one. + #[inline] + pub fn inverse(&self) -> Self { + let inv_rot = self.rotation.inverse(); + Isometry2d { + rotation: inv_rot, + translation: inv_rot * -self.translation, + } + } + + /// Compute `iso1.inverse() * iso2` in a more efficient way for one-shot cases. + /// + /// If the same isometry is used multiple times, it is more efficient to instead compute + /// the inverse once and use that for each transformation. + #[inline] + pub fn inverse_mul(&self, rhs: Self) -> Self { + let inv_rot = self.rotation.inverse(); + let delta_translation = rhs.translation - self.translation; + Self::new(inv_rot * delta_translation, inv_rot * rhs.rotation) + } + + /// Transform a point by rotating and translating it using this isometry. + #[inline] + pub fn transform_point(&self, point: Vec2) -> Vec2 { + self.rotation * point + self.translation + } + + /// Transform a point by rotating and translating it using the inverse of this isometry. + /// + /// This is more efficient than `iso.inverse().transform_point(point)` for one-shot cases. + /// If the same isometry is used multiple times, it is more efficient to instead compute + /// the inverse once and use that for each transformation. + #[inline] + pub fn inverse_transform_point(&self, point: Vec2) -> Vec2 { + self.rotation.inverse() * (point - self.translation) + } +} + +impl From for Affine2 { + #[inline] + fn from(iso: Isometry2d) -> Self { + Affine2 { + matrix2: iso.rotation.into(), + translation: iso.translation, + } + } +} + +impl Mul for Isometry2d { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Isometry2d { + rotation: self.rotation * rhs.rotation, + translation: self.rotation * rhs.translation + self.translation, + } + } +} + +impl Mul for Isometry2d { + type Output = Vec2; + + #[inline] + fn mul(self, rhs: Vec2) -> Self::Output { + self.transform_point(rhs) + } +} + +impl Mul for Isometry2d { + type Output = Dir2; + + #[inline] + fn mul(self, rhs: Dir2) -> Self::Output { + self.rotation * rhs + } +} + +#[cfg(feature = "approx")] +impl AbsDiffEq for Isometry2d { + type Epsilon = ::Epsilon; + + fn default_epsilon() -> Self::Epsilon { + f32::default_epsilon() + } + + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + self.rotation.abs_diff_eq(&other.rotation, epsilon) + && self.translation.abs_diff_eq(other.translation, epsilon) + } +} + +#[cfg(feature = "approx")] +impl RelativeEq for Isometry2d { + fn default_max_relative() -> Self::Epsilon { + Self::default_epsilon() + } + + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.rotation + .relative_eq(&other.rotation, epsilon, max_relative) + && self + .translation + .relative_eq(&other.translation, epsilon, max_relative) + } +} + +#[cfg(feature = "approx")] +impl UlpsEq for Isometry2d { + fn default_max_ulps() -> u32 { + 4 + } + + fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + self.rotation.ulps_eq(&other.rotation, epsilon, max_ulps) + && self + .translation + .ulps_eq(&other.translation, epsilon, max_ulps) + } +} + +/// An isometry in three dimensions, representing a rotation followed by a translation. +/// This can often be useful for expressing relative positions and transformations from one position to another. +/// +/// In particular, this type represents a distance-preserving transformation known as a *rigid motion* or a *direct motion*, +/// and belongs to the special [Euclidean group] SE(3). This includes translation and rotation, but excludes reflection. +/// +/// For the two-dimensional version, see [`Isometry2d`]. +/// +/// [Euclidean group]: https://en.wikipedia.org/wiki/Euclidean_group +/// +/// # Example +/// +/// Isometries can be created from a given translation and rotation: +/// +/// ``` +/// # use bevy_math::{Isometry3d, Quat, Vec3}; +/// # use core::f32::consts::FRAC_PI_2; +/// # +/// let iso = Isometry3d::new(Vec3::new(2.0, 1.0, 3.0), Quat::from_rotation_z(FRAC_PI_2)); +/// ``` +/// +/// Or from separate parts: +/// +/// ``` +/// # use bevy_math::{Isometry3d, Quat, Vec3}; +/// # use core::f32::consts::FRAC_PI_2; +/// # +/// let iso1 = Isometry3d::from_translation(Vec3::new(2.0, 1.0, 3.0)); +/// let iso2 = Isometry3d::from_rotation(Quat::from_rotation_z(FRAC_PI_2)); +/// ``` +/// +/// The isometries can be used to transform points: +/// +/// ``` +/// # use approx::assert_relative_eq; +/// # use bevy_math::{Isometry3d, Quat, Vec3}; +/// # use core::f32::consts::FRAC_PI_2; +/// # +/// let iso = Isometry3d::new(Vec3::new(2.0, 1.0, 3.0), Quat::from_rotation_z(FRAC_PI_2)); +/// let point = Vec3::new(4.0, 4.0, 4.0); +/// +/// // These are equivalent +/// let result = iso.transform_point(point); +/// let result = iso * point; +/// +/// assert_relative_eq!(result, Vec3::new(-2.0, 5.0, 7.0)); +/// ``` +/// +/// Isometries can also be composed together: +/// +/// ``` +/// # use bevy_math::{Isometry3d, Quat, Vec3}; +/// # use core::f32::consts::FRAC_PI_2; +/// # +/// # let iso = Isometry3d::new(Vec3::new(2.0, 1.0, 3.0), Quat::from_rotation_z(FRAC_PI_2)); +/// # let iso1 = Isometry3d::from_translation(Vec3::new(2.0, 1.0, 3.0)); +/// # let iso2 = Isometry3d::from_rotation(Quat::from_rotation_z(FRAC_PI_2)); +/// # +/// assert_eq!(iso1 * iso2, iso); +/// ``` +/// +/// One common operation is to compute an isometry representing the relative positions of two objects +/// for things like intersection tests. This can be done with an inverse transformation: +/// +/// ``` +/// # use bevy_math::{Isometry3d, Quat, Vec3}; +/// # use core::f32::consts::FRAC_PI_2; +/// # +/// let sphere_iso = Isometry3d::from_translation(Vec3::new(2.0, 1.0, 3.0)); +/// let cuboid_iso = Isometry3d::from_rotation(Quat::from_rotation_z(FRAC_PI_2)); +/// +/// // Compute the relative position and orientation between the two shapes +/// let relative_iso = sphere_iso.inverse() * cuboid_iso; +/// +/// // Or alternatively, to skip an extra rotation operation: +/// let relative_iso = sphere_iso.inverse_mul(cuboid_iso); +/// ``` +#[derive(Copy, Clone, Default, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Default) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] +pub struct Isometry3d { + /// The rotational part of a three-dimensional isometry. + pub rotation: Quat, + /// The translational part of a three-dimensional isometry. + pub translation: Vec3A, +} + +impl Isometry3d { + /// The identity isometry which represents the rigid motion of not doing anything. + pub const IDENTITY: Self = Isometry3d { + rotation: Quat::IDENTITY, + translation: Vec3A::ZERO, + }; + + /// Create a three-dimensional isometry from a rotation and a translation. + #[inline] + pub fn new(translation: impl Into, rotation: Quat) -> Self { + Isometry3d { + rotation, + translation: translation.into(), + } + } + + /// Create a three-dimensional isometry from a rotation. + #[inline] + pub fn from_rotation(rotation: Quat) -> Self { + Isometry3d { + rotation, + translation: Vec3A::ZERO, + } + } + + /// Create a three-dimensional isometry from a translation. + #[inline] + pub fn from_translation(translation: impl Into) -> Self { + Isometry3d { + rotation: Quat::IDENTITY, + translation: translation.into(), + } + } + + /// Create a three-dimensional isometry from a translation with the given `x`, `y`, and `z` components. + #[inline] + pub fn from_xyz(x: f32, y: f32, z: f32) -> Self { + Isometry3d { + rotation: Quat::IDENTITY, + translation: Vec3A::new(x, y, z), + } + } + + /// The inverse isometry that undoes this one. + #[inline] + pub fn inverse(&self) -> Self { + let inv_rot = self.rotation.inverse(); + Isometry3d { + rotation: inv_rot, + translation: inv_rot * -self.translation, + } + } + + /// Compute `iso1.inverse() * iso2` in a more efficient way for one-shot cases. + /// + /// If the same isometry is used multiple times, it is more efficient to instead compute + /// the inverse once and use that for each transformation. + #[inline] + pub fn inverse_mul(&self, rhs: Self) -> Self { + let inv_rot = self.rotation.inverse(); + let delta_translation = rhs.translation - self.translation; + Self::new(inv_rot * delta_translation, inv_rot * rhs.rotation) + } + + /// Transform a point by rotating and translating it using this isometry. + #[inline] + pub fn transform_point(&self, point: impl Into) -> Vec3A { + self.rotation * point.into() + self.translation + } + + /// Transform a point by rotating and translating it using the inverse of this isometry. + /// + /// This is more efficient than `iso.inverse().transform_point(point)` for one-shot cases. + /// If the same isometry is used multiple times, it is more efficient to instead compute + /// the inverse once and use that for each transformation. + #[inline] + pub fn inverse_transform_point(&self, point: impl Into) -> Vec3A { + self.rotation.inverse() * (point.into() - self.translation) + } +} + +impl From for Affine3 { + #[inline] + fn from(iso: Isometry3d) -> Self { + Affine3 { + matrix3: Mat3::from_quat(iso.rotation), + translation: iso.translation.into(), + } + } +} + +impl From for Affine3A { + #[inline] + fn from(iso: Isometry3d) -> Self { + Affine3A { + matrix3: Mat3A::from_quat(iso.rotation), + translation: iso.translation, + } + } +} + +impl Mul for Isometry3d { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Isometry3d { + rotation: self.rotation * rhs.rotation, + translation: self.rotation * rhs.translation + self.translation, + } + } +} + +impl Mul for Isometry3d { + type Output = Vec3A; + + #[inline] + fn mul(self, rhs: Vec3A) -> Self::Output { + self.transform_point(rhs) + } +} + +impl Mul for Isometry3d { + type Output = Vec3; + + #[inline] + fn mul(self, rhs: Vec3) -> Self::Output { + self.transform_point(rhs).into() + } +} + +impl Mul for Isometry3d { + type Output = Dir3; + + #[inline] + fn mul(self, rhs: Dir3) -> Self::Output { + self.rotation * rhs + } +} + +#[cfg(feature = "approx")] +impl AbsDiffEq for Isometry3d { + type Epsilon = ::Epsilon; + + fn default_epsilon() -> Self::Epsilon { + f32::default_epsilon() + } + + fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool { + self.rotation.abs_diff_eq(other.rotation, epsilon) + && self.translation.abs_diff_eq(other.translation, epsilon) + } +} + +#[cfg(feature = "approx")] +impl RelativeEq for Isometry3d { + fn default_max_relative() -> Self::Epsilon { + Self::default_epsilon() + } + + fn relative_eq( + &self, + other: &Self, + epsilon: Self::Epsilon, + max_relative: Self::Epsilon, + ) -> bool { + self.rotation + .relative_eq(&other.rotation, epsilon, max_relative) + && self + .translation + .relative_eq(&other.translation, epsilon, max_relative) + } +} + +#[cfg(feature = "approx")] +impl UlpsEq for Isometry3d { + fn default_max_ulps() -> u32 { + 4 + } + + fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool { + self.rotation.ulps_eq(&other.rotation, epsilon, max_ulps) + && self + .translation + .ulps_eq(&other.translation, epsilon, max_ulps) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{vec2, vec3, vec3a}; + use approx::assert_abs_diff_eq; + use std::f32::consts::{FRAC_PI_2, FRAC_PI_3}; + + #[test] + fn mul_2d() { + let iso1 = Isometry2d::new(vec2(1.0, 0.0), Rot2::FRAC_PI_2); + let iso2 = Isometry2d::new(vec2(0.0, 1.0), Rot2::FRAC_PI_2); + let expected = Isometry2d::new(vec2(0.0, 0.0), Rot2::PI); + assert_abs_diff_eq!(iso1 * iso2, expected); + } + + #[test] + fn inverse_mul_2d() { + let iso1 = Isometry2d::new(vec2(1.0, 0.0), Rot2::FRAC_PI_2); + let iso2 = Isometry2d::new(vec2(0.0, 0.0), Rot2::PI); + let expected = Isometry2d::new(vec2(0.0, 1.0), Rot2::FRAC_PI_2); + assert_abs_diff_eq!(iso1.inverse_mul(iso2), expected); + } + + #[test] + fn mul_3d() { + let iso1 = Isometry3d::new(vec3(1.0, 0.0, 0.0), Quat::from_rotation_x(FRAC_PI_2)); + let iso2 = Isometry3d::new(vec3(0.0, 1.0, 0.0), Quat::IDENTITY); + let expected = Isometry3d::new(vec3(1.0, 0.0, 1.0), Quat::from_rotation_x(FRAC_PI_2)); + assert_abs_diff_eq!(iso1 * iso2, expected); + } + + #[test] + fn inverse_mul_3d() { + let iso1 = Isometry3d::new(vec3(1.0, 0.0, 0.0), Quat::from_rotation_x(FRAC_PI_2)); + let iso2 = Isometry3d::new(vec3(1.0, 0.0, 1.0), Quat::from_rotation_x(FRAC_PI_2)); + let expected = Isometry3d::new(vec3(0.0, 1.0, 0.0), Quat::IDENTITY); + assert_abs_diff_eq!(iso1.inverse_mul(iso2), expected); + } + + #[test] + fn identity_2d() { + let iso = Isometry2d::new(vec2(-1.0, -0.5), Rot2::degrees(75.0)); + assert_abs_diff_eq!(Isometry2d::IDENTITY * iso, iso); + assert_abs_diff_eq!(iso * Isometry2d::IDENTITY, iso); + } + + #[test] + fn identity_3d() { + let iso = Isometry3d::new(vec3(-1.0, 2.5, 3.3), Quat::from_rotation_z(FRAC_PI_3)); + assert_abs_diff_eq!(Isometry3d::IDENTITY * iso, iso); + assert_abs_diff_eq!(iso * Isometry3d::IDENTITY, iso); + } + + #[test] + fn inverse_2d() { + let iso = Isometry2d::new(vec2(-1.0, -0.5), Rot2::degrees(75.0)); + let inv = iso.inverse(); + assert_abs_diff_eq!(iso * inv, Isometry2d::IDENTITY); + assert_abs_diff_eq!(inv * iso, Isometry2d::IDENTITY); + } + + #[test] + fn inverse_3d() { + let iso = Isometry3d::new(vec3(-1.0, 2.5, 3.3), Quat::from_rotation_z(FRAC_PI_3)); + let inv = iso.inverse(); + assert_abs_diff_eq!(iso * inv, Isometry3d::IDENTITY); + assert_abs_diff_eq!(inv * iso, Isometry3d::IDENTITY); + } + + #[test] + fn transform_2d() { + let iso = Isometry2d::new(vec2(0.5, -0.5), Rot2::FRAC_PI_2); + let point = vec2(1.0, 1.0); + assert_abs_diff_eq!(vec2(-0.5, 0.5), iso * point); + } + + #[test] + fn inverse_transform_2d() { + let iso = Isometry2d::new(vec2(0.5, -0.5), Rot2::FRAC_PI_2); + let point = vec2(-0.5, 0.5); + assert_abs_diff_eq!(vec2(1.0, 1.0), iso.inverse_transform_point(point)); + } + + #[test] + fn transform_3d() { + let iso = Isometry3d::new(vec3(1.0, 0.0, 0.0), Quat::from_rotation_y(FRAC_PI_2)); + let point = vec3(1.0, 1.0, 1.0); + assert_abs_diff_eq!(vec3(2.0, 1.0, -1.0), iso * point); + } + + #[test] + fn inverse_transform_3d() { + let iso = Isometry3d::new(vec3(1.0, 0.0, 0.0), Quat::from_rotation_y(FRAC_PI_2)); + let point = vec3(2.0, 1.0, -1.0); + assert_abs_diff_eq!(vec3a(1.0, 1.0, 1.0), iso.inverse_transform_point(point)); + } +} diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index e41d0acc49462c..b765a2759e751c 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -17,8 +17,11 @@ pub mod bounding; pub mod common_traits; mod compass; pub mod cubic_splines; +pub mod curve; mod direction; mod float_ord; +mod isometry; +mod ops; pub mod primitives; mod ray; mod rects; @@ -32,6 +35,8 @@ pub use aspect_ratio::AspectRatio; pub use common_traits::*; pub use direction::*; pub use float_ord::*; +pub use isometry::{Isometry2d, Isometry3d}; +pub use ops::*; pub use ray::{Ray2d, Ray3d}; pub use rects::*; pub use rotation2d::Rot2; @@ -47,8 +52,8 @@ pub mod prelude { pub use crate::{ cubic_splines::{ CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator, - CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, RationalCurve, - RationalGenerator, RationalSegment, + CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator, + RationalCurve, RationalGenerator, RationalSegment, }, direction::{Dir2, Dir3, Dir3A}, primitives::*, diff --git a/crates/bevy_math/src/ops.rs b/crates/bevy_math/src/ops.rs new file mode 100644 index 00000000000000..9a40e8434e24cb --- /dev/null +++ b/crates/bevy_math/src/ops.rs @@ -0,0 +1,469 @@ +//! This mod re-exports the correct versions of floating-point operations with +//! unspecified precision in the standard library depending on whether the `libm` +//! crate feature is enabled. +//! +//! All the functions here are named according to their versions in the standard +//! library. + +#![allow(dead_code)] +#![allow(clippy::disallowed_methods)] + +// Note: There are some Rust methods with unspecified precision without a `libm` +// equivalent: +// - `f32::powi` (integer powers) +// - `f32::log` (logarithm with specified base) +// - `f32::abs_sub` (actually unsure if `libm` has this, but don't use it regardless) +// +// Additionally, the following nightly API functions are not presently integrated +// into this, but they would be candidates once standardized: +// - `f32::gamma` +// - `f32::ln_gamma` + +#[cfg(not(feature = "libm"))] +mod std_ops { + + /// Raises a number to a floating point power. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn powf(x: f32, y: f32) -> f32 { + f32::powf(x, y) + } + + /// Returns `e^(self)`, (the exponential function). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn exp(x: f32) -> f32 { + f32::exp(x) + } + + /// Returns `2^(self)`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn exp2(x: f32) -> f32 { + f32::exp2(x) + } + + /// Returns the natural logarithm of the number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn ln(x: f32) -> f32 { + f32::ln(x) + } + + /// Returns the base 2 logarithm of the number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn log2(x: f32) -> f32 { + f32::log2(x) + } + + /// Returns the base 10 logarithm of the number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn log10(x: f32) -> f32 { + f32::log10(x) + } + + /// Returns the cube root of a number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn cbrt(x: f32) -> f32 { + f32::cbrt(x) + } + + /// Compute the distance between the origin and a point `(x, y)` on the Euclidean plane. + /// Equivalently, compute the length of the hypotenuse of a right-angle triangle with other sides having length `x.abs()` and `y.abs()`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn hypot(x: f32, y: f32) -> f32 { + f32::hypot(x, y) + } + + /// Computes the sine of a number (in radians). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sin(x: f32) -> f32 { + f32::sin(x) + } + + /// Computes the cosine of a number (in radians). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn cos(x: f32) -> f32 { + f32::cos(x) + } + + /// Computes the tangent of a number (in radians). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn tan(x: f32) -> f32 { + f32::tan(x) + } + + /// Computes the arcsine of a number. Return value is in radians in + /// the range [-pi/2, pi/2] or NaN if the number is outside the range + /// [-1, 1]. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn asin(x: f32) -> f32 { + f32::asin(x) + } + + /// Computes the arccosine of a number. Return value is in radians in + /// the range [0, pi] or NaN if the number is outside the range + /// [-1, 1]. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn acos(x: f32) -> f32 { + f32::acos(x) + } + + /// Computes the arctangent of a number. Return value is in radians in the + /// range [-pi/2, pi/2]; + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn atan(x: f32) -> f32 { + f32::atan(x) + } + + /// Computes the four quadrant arctangent of `self` (`y`) and `other` (`x`) in radians. + /// + /// * `x = 0`, `y = 0`: `0` + /// * `x >= 0`: `arctan(y/x)` -> `[-pi/2, pi/2]` + /// * `y >= 0`: `arctan(y/x) + pi` -> `(pi/2, pi]` + /// * `y < 0`: `arctan(y/x) - pi` -> `(-pi, -pi/2)` + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn atan2(x: f32, y: f32) -> f32 { + f32::atan2(x, y) + } + + /// Simultaneously computes the sine and cosine of the number, `x`. Returns + /// `(sin(x), cos(x))`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sin_cos(x: f32) -> (f32, f32) { + f32::sin_cos(x) + } + + /// Returns `e^(self) - 1` in a way that is accurate even if the + /// number is close to zero. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn exp_m1(x: f32) -> f32 { + f32::exp_m1(x) + } + + /// Returns `ln(1+n)` (natural logarithm) more accurately than if + /// the operations were performed separately. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn ln_1p(x: f32) -> f32 { + f32::ln_1p(x) + } + + /// Hyperbolic sine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sinh(x: f32) -> f32 { + f32::sinh(x) + } + + /// Hyperbolic cosine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn cosh(x: f32) -> f32 { + f32::cosh(x) + } + + /// Hyperbolic tangent function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn tanh(x: f32) -> f32 { + f32::tanh(x) + } + + /// Inverse hyperbolic sine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn asinh(x: f32) -> f32 { + f32::asinh(x) + } + + /// Inverse hyperbolic cosine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn acosh(x: f32) -> f32 { + f32::acosh(x) + } + + /// Inverse hyperbolic tangent function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn atanh(x: f32) -> f32 { + f32::atanh(x) + } +} + +#[cfg(feature = "libm")] +mod libm_ops { + + /// Raises a number to a floating point power. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn powf(x: f32, y: f32) -> f32 { + libm::powf(x, y) + } + + /// Returns `e^(self)`, (the exponential function). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn exp(x: f32) -> f32 { + libm::expf(x) + } + + /// Returns `2^(self)`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn exp2(x: f32) -> f32 { + libm::exp2f(x) + } + + /// Returns the natural logarithm of the number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn ln(x: f32) -> f32 { + // This isn't documented in `libm` but this is actually the base e logarithm. + libm::logf(x) + } + + /// Returns the base 2 logarithm of the number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn log2(x: f32) -> f32 { + libm::log2f(x) + } + + /// Returns the base 10 logarithm of the number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn log10(x: f32) -> f32 { + libm::log10f(x) + } + + /// Returns the cube root of a number. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn cbrt(x: f32) -> f32 { + libm::cbrtf(x) + } + + /// Compute the distance between the origin and a point `(x, y)` on the Euclidean plane. + /// Equivalently, compute the length of the hypotenuse of a right-angle triangle with other sides having length `x.abs()` and `y.abs()`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn hypot(x: f32, y: f32) -> f32 { + libm::hypotf(x, y) + } + + /// Computes the sine of a number (in radians). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sin(x: f32) -> f32 { + libm::sinf(x) + } + + /// Computes the cosine of a number (in radians). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn cos(x: f32) -> f32 { + libm::cosf(x) + } + + /// Computes the tangent of a number (in radians). + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn tan(x: f32) -> f32 { + libm::tanf(x) + } + + /// Computes the arcsine of a number. Return value is in radians in + /// the range [-pi/2, pi/2] or NaN if the number is outside the range + /// [-1, 1]. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn asin(x: f32) -> f32 { + libm::asinf(x) + } + + /// Computes the arccosine of a number. Return value is in radians in + /// Hyperbolic tangent function. + /// + /// Precision is specified when the `libm` feature is enabled. + /// the range [0, pi] or NaN if the number is outside the range + /// [-1, 1]. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn acos(x: f32) -> f32 { + libm::acosf(x) + } + + /// Computes the arctangent of a number. Return value is in radians in the + /// range [-pi/2, pi/2]; + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn atan(x: f32) -> f32 { + libm::atanf(x) + } + + /// Computes the four quadrant arctangent of `self` (`y`) and `other` (`x`) in radians. + /// + /// * `x = 0`, `y = 0`: `0` + /// * `x >= 0`: `arctan(y/x)` -> `[-pi/2, pi/2]` + /// * `y >= 0`: `arctan(y/x) + pi` -> `(pi/2, pi]` + /// * `y < 0`: `arctan(y/x) - pi` -> `(-pi, -pi/2)` + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn atan2(x: f32, y: f32) -> f32 { + libm::atan2f(x, y) + } + + /// Simultaneously computes the sine and cosine of the number, `x`. Returns + /// `(sin(x), cos(x))`. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sin_cos(x: f32) -> (f32, f32) { + libm::sincosf(x) + } + + /// Returns `e^(self) - 1` in a way that is accurate even if the + /// number is close to zero. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn exp_m1(x: f32) -> f32 { + libm::expm1f(x) + } + + /// Returns `ln(1+n)` (natural logarithm) more accurately than if + /// the operations were performed separately. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn ln_1p(x: f32) -> f32 { + libm::log1pf(x) + } + + /// Hyperbolic sine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn sinh(x: f32) -> f32 { + libm::sinhf(x) + } + + /// Hyperbolic cosine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn cosh(x: f32) -> f32 { + libm::coshf(x) + } + + /// Hyperbolic tangent function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn tanh(x: f32) -> f32 { + libm::tanhf(x) + } + + /// Inverse hyperbolic sine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn asinh(x: f32) -> f32 { + libm::asinhf(x) + } + + /// Inverse hyperbolic cosine function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn acosh(x: f32) -> f32 { + libm::acoshf(x) + } + + /// Inverse hyperbolic tangent function. + /// + /// Precision is specified when the `libm` feature is enabled. + #[inline(always)] + pub fn atanh(x: f32) -> f32 { + libm::atanhf(x) + } +} + +#[cfg(feature = "libm")] +pub use libm_ops::*; +#[cfg(not(feature = "libm"))] +pub use std_ops::*; + +/// This extension trait covers shortfall in determinacy from the lack of a `libm` counterpart +/// to `f32::powi`. Use this for the common small exponents. +pub trait FloatPow { + /// Squares the f32 + fn squared(self) -> Self; + /// Cubes the f32 + fn cubed(self) -> Self; +} + +impl FloatPow for f32 { + #[inline] + fn squared(self) -> Self { + self * self + } + #[inline] + fn cubed(self) -> Self { + self * self * self + } +} diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index e96160367802bf..f43541809b9700 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,7 +1,10 @@ -use std::f32::consts::{FRAC_PI_2, FRAC_PI_3, PI}; +use std::f32::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, PI}; use super::{Measured2d, Primitive2d, WindingOrder}; -use crate::{Dir2, Vec2}; +use crate::{ + ops::{self, FloatPow}, + Dir2, Vec2, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -54,7 +57,7 @@ impl Circle { pub fn closest_point(&self, point: Vec2) -> Vec2 { let distance_squared = point.length_squared(); - if distance_squared <= self.radius.powi(2) { + if distance_squared <= self.radius.squared() { // The point is inside the circle. point } else { @@ -70,7 +73,7 @@ impl Measured2d for Circle { /// Get the area of the circle #[inline(always)] fn area(&self) -> f32 { - PI * self.radius.powi(2) + PI * self.radius.squared() } /// Get the perimeter or circumference of the circle @@ -200,7 +203,7 @@ impl Arc2d { /// Get half the distance between the endpoints (half the length of the chord) #[inline(always)] pub fn half_chord_length(&self) -> f32 { - self.radius * f32::sin(self.half_angle) + self.radius * ops::sin(self.half_angle) } /// Get the distance between the endpoints (the length of the chord) @@ -226,7 +229,7 @@ impl Arc2d { // used by Wolfram MathWorld, which is the distance rather than the segment. pub fn apothem(&self) -> f32 { let sign = if self.is_minor() { 1.0 } else { -1.0 }; - sign * f32::sqrt(self.radius.powi(2) - self.half_chord_length().powi(2)) + sign * f32::sqrt(self.radius.squared() - self.half_chord_length().squared()) } /// Get the length of the sagitta of this arc, that is, @@ -388,7 +391,7 @@ impl CircularSector { /// Returns the area of this sector #[inline(always)] pub fn area(&self) -> f32 { - self.arc.radius.powi(2) * self.arc.half_angle + self.arc.radius.squared() * self.arc.half_angle } } @@ -527,7 +530,7 @@ impl CircularSegment { /// Returns the area of this segment #[inline(always)] pub fn area(&self) -> f32 { - 0.5 * self.arc.radius.powi(2) * (self.arc.angle() - self.arc.angle().sin()) + 0.5 * self.arc.radius.squared() * (self.arc.angle() - ops::sin(self.arc.angle())) } } @@ -882,11 +885,11 @@ impl Measured2d for Ellipse { // The algorithm used here is the Gauss-Kummer infinite series expansion of the elliptic integral expression for the perimeter of ellipses // For more information see https://www.wolframalpha.com/input/?i=gauss-kummer+series // We only use the terms up to `i == 20` for this approximation - let h = ((a - b) / (a + b)).powi(2); + let h = ((a - b) / (a + b)).squared(); PI * (a + b) * (0..=20) - .map(|i| BINOMIAL_COEFFICIENTS[i] * h.powi(i as i32)) + .map(|i| BINOMIAL_COEFFICIENTS[i] * ops::powf(h, i as f32)) .sum::() } } @@ -953,8 +956,8 @@ impl Annulus { pub fn closest_point(&self, point: Vec2) -> Vec2 { let distance_squared = point.length_squared(); - if self.inner_circle.radius.powi(2) <= distance_squared { - if distance_squared <= self.outer_circle.radius.powi(2) { + if self.inner_circle.radius.squared() <= distance_squared { + if distance_squared <= self.outer_circle.radius.squared() { // The point is inside the annulus. point } else { @@ -976,7 +979,7 @@ impl Measured2d for Annulus { /// Get the area of the annulus #[inline(always)] fn area(&self) -> f32 { - PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) + PI * (self.outer_circle.radius.squared() - self.inner_circle.radius.squared()) } /// Get the perimeter or circumference of the annulus, @@ -1031,7 +1034,7 @@ impl Rhombus { #[inline(always)] pub fn from_side(side: f32) -> Self { Self { - half_diagonals: Vec2::splat(side.hypot(side) / 2.0), + half_diagonals: Vec2::splat(side * FRAC_1_SQRT_2), } } @@ -1694,13 +1697,13 @@ impl RegularPolygon { #[inline(always)] #[doc(alias = "apothem")] pub fn inradius(&self) -> f32 { - self.circumradius() * (PI / self.sides as f32).cos() + self.circumradius() * ops::cos(PI / self.sides as f32) } /// Get the length of one side of the regular polygon #[inline(always)] pub fn side_length(&self) -> f32 { - 2.0 * self.circumradius() * (PI / self.sides as f32).sin() + 2.0 * self.circumradius() * ops::sin(PI / self.sides as f32) } /// Get the internal angle of the regular polygon in degrees. @@ -1745,12 +1748,12 @@ impl RegularPolygon { /// 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 start_angle = rotation + 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(); + let (sin, cos) = ops::sin_cos(theta); Vec2::new(cos, sin) * self.circumcircle.radius }) } @@ -1761,7 +1764,7 @@ impl Measured2d for RegularPolygon { #[inline(always)] fn area(&self) -> f32 { let angle: f32 = 2.0 * PI / (self.sides as f32); - (self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0 + (self.sides as f32) * self.circumradius().squared() * ops::sin(angle) / 2.0 } /// Get the perimeter of the regular polygon. @@ -1821,7 +1824,7 @@ mod tests { // Reference values were computed by hand and/or with external tools use super::*; - use approx::assert_relative_eq; + use approx::{assert_abs_diff_eq, assert_relative_eq}; #[test] fn rectangle_closest_point() { @@ -1913,10 +1916,10 @@ mod tests { assert_eq!(rhombus.inradius(), 0.0, "incorrect inradius"); assert_eq!(rhombus.circumradius(), 0.0, "incorrect circumradius"); let rhombus = Rhombus::from_side(std::f32::consts::SQRT_2); - assert_eq!(rhombus, Rhombus::new(2.0, 2.0)); - assert_eq!( - rhombus, - Rhombus::from_inradius(std::f32::consts::FRAC_1_SQRT_2) + assert_abs_diff_eq!(rhombus.half_diagonals, Vec2::new(1.0, 1.0)); + assert_abs_diff_eq!( + rhombus.half_diagonals, + Rhombus::from_inradius(FRAC_1_SQRT_2).half_diagonals ); } @@ -2069,7 +2072,7 @@ mod tests { 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; + let side_sistance = 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 5eb456f37c101c..7ee6a82d6f2411 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,12 +1,13 @@ use std::f32::consts::{FRAC_PI_3, PI}; use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; -use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3}; +use crate::{ops, ops::FloatPow, Dir3, InvalidDirectionError, Isometry3d, Mat3, Vec2, Vec3}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use glam::Quat; /// A sphere primitive, representing the set of all points some distance from the origin #[derive(Clone, Copy, Debug, PartialEq)] @@ -54,7 +55,7 @@ impl Sphere { pub fn closest_point(&self, point: Vec3) -> Vec3 { let distance_squared = point.length_squared(); - if distance_squared <= self.radius.powi(2) { + if distance_squared <= self.radius.squared() { // The point is inside the sphere. point } else { @@ -70,13 +71,13 @@ impl Measured3d for Sphere { /// Get the surface area of the sphere #[inline(always)] fn area(&self) -> f32 { - 4.0 * PI * self.radius.powi(2) + 4.0 * PI * self.radius.squared() } /// Get the volume of the sphere #[inline(always)] fn volume(&self) -> f32 { - 4.0 * FRAC_PI_3 * self.radius.powi(3) + 4.0 * FRAC_PI_3 * self.radius.cubed() } } @@ -214,6 +215,115 @@ impl InfinitePlane3d { (Self { normal }, translation) } + + /// Computes the shortest distance between a plane transformed with the given `isometry` and a + /// `point`. The result is a signed value; it's positive if the point lies in the half-space + /// that the plane's normal vector points towards. + #[inline] + pub fn signed_distance(&self, isometry: Isometry3d, point: Vec3) -> f32 { + self.normal.dot(isometry.inverse() * point) + } + + /// Injects the `point` into this plane transformed with the given `isometry`. + /// + /// This projects the point orthogonally along the shortest path onto the plane. + #[inline] + pub fn project_point(&self, isometry: Isometry3d, point: Vec3) -> Vec3 { + point - self.normal * self.signed_distance(isometry, point) + } + + /// Computes an [`Isometry3d`] which transforms points from the plane in 3D space with the given + /// `origin` to the XY-plane. + /// + /// ## Guarantees + /// + /// * the transformation is a [congruence] meaning it will preserve all distances and angles of + /// the transformed geometry + /// * uses the least rotation possible to transform the geometry + /// * if two geometries are transformed with the same isometry, then the relations between + /// them, like distances, are also preserved + /// * compared to projections, the transformation is lossless (up to floating point errors) + /// reversible + /// + /// ## Non-Guarantees + /// + /// * the rotation used is generally not unique + /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to + /// enforce some kind of alignment the user has to use an extra transformation ontop of this + /// one + /// + /// See [`isometries_xy`] for example usescases. + /// + /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry) + /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy` + #[inline] + pub fn isometry_into_xy(&self, origin: Vec3) -> Isometry3d { + let rotation = Quat::from_rotation_arc(self.normal.as_vec3(), Vec3::Z); + let transformed_origin = rotation * origin; + Isometry3d::new(-Vec3::Z * transformed_origin.z, rotation) + } + + /// Computes an [`Isometry3d`] which transforms points from the XY-plane to this plane with the + /// given `origin`. + /// + /// ## Guarantees + /// + /// * the transformation is a [congruence] meaning it will preserve all distances and angles of + /// the transformed geometry + /// * uses the least rotation possible to transform the geometry + /// * if two geometries are transformed with the same isometry, then the relations between + /// them, like distances, are also preserved + /// * compared to projections, the transformation is lossless (up to floating point errors) + /// reversible + /// + /// ## Non-Guarantees + /// + /// * the rotation used is generally not unique + /// * the orientation of the transformed geometry in the XY plane might be arbitrary, to + /// enforce some kind of alignment the user has to use an extra transformation ontop of this + /// one + /// + /// See [`isometries_xy`] for example usescases. + /// + /// [congruence]: https://en.wikipedia.org/wiki/Congruence_(geometry) + /// [`isometries_xy`]: `InfinitePlane3d::isometries_xy` + #[inline] + pub fn isometry_from_xy(&self, origin: Vec3) -> Isometry3d { + self.isometry_into_xy(origin).inverse() + } + + /// Computes both [isometries] which transforms points from the plane in 3D space with the + /// given `origin` to the XY-plane and back. + /// + /// [isometries]: `Isometry3d` + /// + /// # Example + /// + /// The projection and its inverse can be used to run 2D algorithms on flat shapes in 3D. The + /// workflow would usually look like this: + /// + /// ``` + /// # use bevy_math::{Vec3, Dir3}; + /// # use bevy_math::primitives::InfinitePlane3d; + /// + /// let triangle_3d @ [a, b, c] = [Vec3::X, Vec3::Y, Vec3::Z]; + /// let center = (a + b + c) / 3.0; + /// + /// let plane = InfinitePlane3d::new(Vec3::ONE); + /// + /// let (to_xy, from_xy) = plane.isometries_xy(center); + /// + /// let triangle_2d = triangle_3d.map(|vec3| to_xy * vec3).map(|vec3| vec3.truncate()); + /// + /// // apply some algorithm to `triangle_2d` + /// + /// let triangle_3d = triangle_2d.map(|vec2| vec2.extend(0.0)).map(|vec3| from_xy * vec3); + /// ``` + #[inline] + pub fn isometries_xy(&self, origin: Vec3) -> (Isometry3d, Isometry3d) { + let projection = self.isometry_into_xy(origin); + (projection, projection.inverse()) + } } /// An infinite line going through the origin along a direction in 3D space. @@ -505,7 +615,7 @@ impl Cylinder { /// Get the surface area of one base of the cylinder #[inline(always)] pub fn base_area(&self) -> f32 { - PI * self.radius.powi(2) + PI * self.radius.squared() } } @@ -625,6 +735,10 @@ impl Default for Cone { } impl Cone { + /// Create a new [`Cone`] from a radius and height. + pub fn new(radius: f32, height: f32) -> Self { + Self { radius, height } + } /// Get the base of the cone as a [`Circle`] #[inline(always)] pub fn base(&self) -> Circle { @@ -638,7 +752,7 @@ impl Cone { #[inline(always)] #[doc(alias = "side_length")] pub fn slant_height(&self) -> f32 { - self.radius.hypot(self.height) + ops::hypot(self.radius, self.height) } /// Get the surface area of the side of the cone, @@ -652,7 +766,7 @@ impl Cone { /// Get the surface area of the base of the cone #[inline(always)] pub fn base_area(&self) -> f32 { - PI * self.radius.powi(2) + PI * self.radius.squared() } } @@ -824,14 +938,14 @@ impl Measured3d for Torus { /// the expected result when the torus has a ring and isn't self-intersecting #[inline(always)] fn area(&self) -> f32 { - 4.0 * PI.powi(2) * self.major_radius * self.minor_radius + 4.0 * PI.squared() * self.major_radius * self.minor_radius } /// Get the volume of the torus. Note that this only produces /// the expected result when the torus has a ring and isn't self-intersecting #[inline(always)] fn volume(&self) -> f32 { - 2.0 * PI.powi(2) * self.major_radius * self.minor_radius.powi(2) + 2.0 * PI.squared() * self.major_radius * self.minor_radius.squared() } } @@ -1253,10 +1367,58 @@ mod tests { } #[test] - fn infinite_plane_from_points() { - let (plane, translation) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); + fn infinite_plane_math() { + let (plane, origin) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); - assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation"); + assert_eq!(origin, Vec3::Z * 0.33333334, "incorrect translation"); + + let point_in_plane = Vec3::X + Vec3::Z; + assert_eq!( + plane.signed_distance(Isometry3d::from_translation(origin), point_in_plane), + 0.0, + "incorrect distance" + ); + assert_eq!( + plane.project_point(Isometry3d::from_translation(origin), point_in_plane), + point_in_plane, + "incorrect point" + ); + + let point_outside = Vec3::Y; + assert_eq!( + plane.signed_distance(Isometry3d::from_translation(origin), point_outside), + -1.0, + "incorrect distance" + ); + assert_eq!( + plane.project_point(Isometry3d::from_translation(origin), point_outside), + Vec3::ZERO, + "incorrect point" + ); + + let point_outside = Vec3::NEG_Y; + assert_eq!( + plane.signed_distance(Isometry3d::from_translation(origin), point_outside), + 1.0, + "incorrect distance" + ); + assert_eq!( + plane.project_point(Isometry3d::from_translation(origin), point_outside), + Vec3::ZERO, + "incorrect point" + ); + + let area_f = |[a, b, c]: [Vec3; 3]| (a - b).cross(a - c).length() * 0.5; + let (proj, inj) = plane.isometries_xy(origin); + + let triangle = [Vec3::X, Vec3::Y, Vec3::ZERO]; + assert_eq!(area_f(triangle), 0.5, "incorrect area"); + + let triangle_proj = triangle.map(|vec3| proj * vec3); + assert_relative_eq!(area_f(triangle_proj), 0.5); + + let triangle_proj_inj = triangle_proj.map(|vec3| inj * vec3); + assert_relative_eq!(area_f(triangle_proj_inj), 0.5); } #[test] diff --git a/crates/bevy_math/src/rotation2d.rs b/crates/bevy_math/src/rotation2d.rs index 77011f93a3cd7b..e9fd1f7f132802 100644 --- a/crates/bevy_math/src/rotation2d.rs +++ b/crates/bevy_math/src/rotation2d.rs @@ -1,6 +1,9 @@ use glam::FloatExt; -use crate::prelude::{Mat2, Vec2}; +use crate::{ + ops, + prelude::{Mat2, Vec2}, +}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -99,14 +102,7 @@ impl Rot2 { /// Creates a [`Rot2`] from a counterclockwise angle in radians. #[inline] pub fn radians(radians: f32) -> Self { - #[cfg(feature = "libm")] - let (sin, cos) = ( - libm::sin(radians as f64) as f32, - libm::cos(radians as f64) as f32, - ); - #[cfg(not(feature = "libm"))] - let (sin, cos) = radians.sin_cos(); - + let (sin, cos) = ops::sin_cos(radians); Self::from_sin_cos(sin, cos) } @@ -136,14 +132,7 @@ impl Rot2 { /// Returns the rotation in radians in the `(-pi, pi]` range. #[inline] pub fn as_radians(self) -> f32 { - #[cfg(feature = "libm")] - { - libm::atan2(self.sin as f64, self.cos as f64) as f32 - } - #[cfg(not(feature = "libm"))] - { - f32::atan2(self.sin, self.cos) - } + ops::atan2(self.sin, self.cos) } /// Returns the rotation in degrees in the `(-180, 180]` range. @@ -226,6 +215,20 @@ impl Rot2 { Self::from_sin_cos(self.sin * length_recip, self.cos * length_recip) } + /// Returns `self` after an approximate normalization, assuming the value is already nearly normalized. + /// Useful for preventing numerical error accumulation. + /// See [`Dir3::fast_renormalize`](crate::Dir3::fast_renormalize) for an example of when such error accumulation might occur. + #[inline] + pub fn fast_renormalize(self) -> Self { + let length_squared = self.length_squared(); + // Based on a Taylor approximation of the inverse square root, see [`Dir3::fast_renormalize`](crate::Dir3::fast_renormalize) for more details. + let length_recip_approx = 0.5 * (3.0 - length_squared); + Rot2 { + sin: self.sin * length_recip_approx, + cos: self.cos * length_recip_approx, + } + } + /// Returns `true` if the rotation is neither infinite nor NaN. #[inline] pub fn is_finite(self) -> bool { @@ -522,6 +525,45 @@ mod tests { assert!(normalized_rotation.is_normalized()); } + #[test] + fn fast_renormalize() { + let rotation = Rot2 { sin: 1.0, cos: 0.5 }; + let normalized_rotation = rotation.normalize(); + + let mut unnormalized_rot = rotation; + let mut renormalized_rot = rotation; + let mut initially_normalized_rot = normalized_rotation; + let mut fully_normalized_rot = normalized_rotation; + + // Compute a 64x (=2⁶) multiple of the rotation. + for _ in 0..6 { + unnormalized_rot = unnormalized_rot * unnormalized_rot; + renormalized_rot = renormalized_rot * renormalized_rot; + initially_normalized_rot = initially_normalized_rot * initially_normalized_rot; + fully_normalized_rot = fully_normalized_rot * fully_normalized_rot; + + renormalized_rot = renormalized_rot.fast_renormalize(); + fully_normalized_rot = fully_normalized_rot.normalize(); + } + + assert!(!unnormalized_rot.is_normalized()); + + assert!(renormalized_rot.is_normalized()); + assert!(fully_normalized_rot.is_normalized()); + + assert_relative_eq!(fully_normalized_rot, renormalized_rot, epsilon = 0.000001); + assert_relative_eq!( + fully_normalized_rot, + unnormalized_rot.normalize(), + epsilon = 0.000001 + ); + assert_relative_eq!( + fully_normalized_rot, + initially_normalized_rot.normalize(), + epsilon = 0.000001 + ); + } + #[test] fn try_normalize() { // Valid diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 325134805ca851..10f9e17f60dd5f 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -40,7 +40,7 @@ use std::f32::consts::{PI, TAU}; -use crate::{primitives::*, NormedVectorSpace, Vec2, Vec3}; +use crate::{ops, primitives::*, NormedVectorSpace, Vec2, Vec3}; use rand::{ distributions::{Distribution, WeightedIndex}, Rng, @@ -155,39 +155,41 @@ impl ShapeSample for Circle { let theta = rng.gen_range(0.0..TAU); let r_squared = rng.gen_range(0.0..=(self.radius * self.radius)); let r = r_squared.sqrt(); - Vec2::new(r * theta.cos(), r * theta.sin()) + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(r * cos, r * sin) } fn sample_boundary(&self, rng: &mut R) -> Vec2 { let theta = rng.gen_range(0.0..TAU); - Vec2::new(self.radius * theta.cos(), self.radius * theta.sin()) + let (sin, cos) = ops::sin_cos(theta); + Vec2::new(self.radius * cos, self.radius * sin) } } +/// Boundary sampling for unit-spheres +#[inline] +fn sample_unit_sphere_boundary(rng: &mut R) -> Vec3 { + let z = rng.gen_range(-1f32..=1f32); + let (a_sin, a_cos) = ops::sin_cos(rng.gen_range(-PI..=PI)); + let c = (1f32 - z * z).sqrt(); + let x = a_sin * c; + let y = a_cos * c; + + Vec3::new(x, y, z) +} + impl ShapeSample for Sphere { type Output = Vec3; fn sample_interior(&self, rng: &mut R) -> Vec3 { - // https://mathworld.wolfram.com/SpherePointPicking.html - let theta = rng.gen_range(0.0..TAU); - let phi = rng.gen_range(-1.0_f32..1.0).acos(); let r_cubed = rng.gen_range(0.0..=(self.radius * self.radius * self.radius)); - let r = r_cubed.cbrt(); - Vec3 { - x: r * phi.sin() * theta.cos(), - y: r * phi.sin() * theta.sin(), - z: r * phi.cos(), - } + let r = ops::cbrt(r_cubed); + + r * sample_unit_sphere_boundary(rng) } fn sample_boundary(&self, rng: &mut R) -> Vec3 { - let theta = rng.gen_range(0.0..TAU); - let phi = rng.gen_range(-1.0_f32..1.0).acos(); - Vec3 { - x: self.radius * phi.sin() * theta.cos(), - y: self.radius * phi.sin() * theta.sin(), - z: self.radius * phi.cos(), - } + self.radius * sample_unit_sphere_boundary(rng) } } @@ -202,8 +204,9 @@ impl ShapeSample for Annulus { let r_squared = rng.gen_range((inner_radius * inner_radius)..(outer_radius * outer_radius)); let r = r_squared.sqrt(); let theta = rng.gen_range(0.0..TAU); + let (sin, cos) = ops::sin_cos(theta); - Vec2::new(r * theta.cos(), r * theta.sin()) + Vec2::new(r * cos, r * sin) } fn sample_boundary(&self, rng: &mut R) -> Self::Output { @@ -624,7 +627,7 @@ mod tests { for _ in 0..5000 { let point = circle.sample_boundary(&mut rng); - let angle = f32::atan(point.y / point.x) + PI / 2.0; + let angle = ops::atan(point.y / point.x) + PI / 2.0; let wedge = (angle * 8.0 / PI).floor() as usize; wedge_hits[wedge] += 1; } diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 4ce612c4e493f1..30372a699774c2 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -16,7 +16,7 @@ keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] rust-version = "1.76.0" [dependencies] -glam = "0.27" +glam = "0.28" [[example]] name = "generate" @@ -25,5 +25,5 @@ name = "generate" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 4c75bbaff24e6f..a4c46dfdbcc608 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -18,13 +18,7 @@ shader_format_glsl = ["bevy_render/shader_format_glsl"] trace = ["bevy_render/trace"] ios_simulator = ["bevy_render/ios_simulator"] # Enables the meshlet renderer for dense high-poly scenes (experimental) -meshlet = [ - "dep:lz4_flex", - "dep:serde", - "dep:bincode", - "dep:thiserror", - "dep:range-alloc", -] +meshlet = ["dep:lz4_flex", "dep:thiserror", "dep:range-alloc", "dep:bevy_tasks"] # Enables processing meshes into meshlet meshes meshlet_processor = ["meshlet", "dep:meshopt", "dep:metis", "dep:itertools"] @@ -34,16 +28,17 @@ bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", ] } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", optional = true } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } # other @@ -53,10 +48,8 @@ fixedbitset = "0.5" lz4_flex = { version = "0.11", default-features = false, features = [ "frame", ], optional = true } -serde = { version = "1", features = ["derive", "rc"], optional = true } -bincode = { version = "1", optional = true } thiserror = { version = "1", optional = true } -range-alloc = { version = "0.1", optional = true } +range-alloc = { version = "0.1.3", optional = true } meshopt = { version = "0.3.0", optional = true } metis = { version = "0.2", optional = true } itertools = { version = "0.13", optional = true } @@ -71,5 +64,5 @@ static_assertions = "1" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 3a6dec734fb72d..e1147a46e4297b 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -3,13 +3,14 @@ use crate::{ StandardMaterial, }; use bevy_asset::Handle; -use bevy_ecs::entity::EntityHashMap; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::entity::{Entity, EntityHashMap}; use bevy_ecs::{bundle::Bundle, component::Component, reflect::ReflectComponent}; use bevy_reflect::Reflect; use bevy_render::{ mesh::Mesh, primitives::{CascadesFrusta, CubemapFrusta, Frustum}, - view::{InheritedVisibility, ViewVisibility, Visibility, VisibleEntities}, + view::{InheritedVisibility, ViewVisibility, Visibility}, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -45,27 +46,37 @@ impl Default for MaterialMeshBundle { } } +/// Collection of mesh entities visible for 3D lighting. +/// This component contains all mesh entities visible from the current light view. +/// The collection is updated automatically by [`crate::SimulationLightSystems`]. +#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Component)] +pub struct VisibleMeshEntities { + #[reflect(ignore)] + pub entities: Vec, +} + #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct CubemapVisibleEntities { #[reflect(ignore)] - data: [VisibleEntities; 6], + data: [VisibleMeshEntities; 6], } impl CubemapVisibleEntities { - pub fn get(&self, i: usize) -> &VisibleEntities { + pub fn get(&self, i: usize) -> &VisibleMeshEntities { &self.data[i] } - pub fn get_mut(&mut self, i: usize) -> &mut VisibleEntities { + pub fn get_mut(&mut self, i: usize) -> &mut VisibleMeshEntities { &mut self.data[i] } - pub fn iter(&self) -> impl DoubleEndedIterator { + pub fn iter(&self) -> impl DoubleEndedIterator { self.data.iter() } - pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { self.data.iter_mut() } } @@ -75,7 +86,7 @@ impl CubemapVisibleEntities { pub struct CascadesVisibleEntities { /// Map of view entity to the visible entities for each cascade frustum. #[reflect(ignore)] - pub entities: EntityHashMap>, + pub entities: EntityHashMap>, } /// A component bundle for [`PointLight`] entities. @@ -98,7 +109,7 @@ pub struct PointLightBundle { #[derive(Debug, Bundle, Default, Clone)] pub struct SpotLightBundle { pub spot_light: SpotLight, - pub visible_entities: VisibleEntities, + pub visible_entities: VisibleMeshEntities, pub frustum: Frustum, pub transform: Transform, pub global_transform: GlobalTransform, diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 9727659bf67cc3..f2a6cdc3aeeda7 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -478,7 +478,7 @@ pub(crate) fn assign_objects_to_clusters( // as they often assume that the widest part of the sphere under projection is the // center point on the axis of interest plus the radius, and that is not true! let view_clusterable_object_sphere = Sphere { - center: Vec3A::from( + center: Vec3A::from_vec4( view_from_world * clusterable_object_sphere.center.extend(1.0), ), radius: clusterable_object_sphere.radius * view_from_world_scale_max, @@ -798,7 +798,7 @@ fn cluster_space_clusterable_object_aabb( clusterable_object_sphere: &Sphere, ) -> (Vec3, Vec3) { let clusterable_object_aabb_view = Aabb { - center: Vec3A::from(view_from_world * clusterable_object_sphere.center.extend(1.0)), + center: Vec3A::from_vec4(view_from_world * clusterable_object_sphere.center.extend(1.0)), half_extents: Vec3A::from(clusterable_object_sphere.radius * view_from_world_scale.abs()), }; let (mut clusterable_object_aabb_view_min, mut clusterable_object_aabb_view_max) = ( diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index a0d628474db8e7..fe913a1d196bb7 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -2,10 +2,11 @@ use std::num::NonZeroU64; +use bevy_core_pipeline::core_3d::Camera3d; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, - query::Without, + query::{With, Without}, reflect::ReflectComponent, system::{Commands, Query, Res, Resource}, world::{FromWorld, World}, @@ -348,7 +349,7 @@ impl Clusters { pub fn add_clusters( mut commands: Commands, - cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), Without>, + cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without, With)>, ) { for (entity, config, camera) in &cameras { if !camera.is_active { diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 4d7f247b8255a7..eb4ba66cf09980 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -1,7 +1,7 @@ use crate::{ graph::NodePbr, irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight, MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, ScreenSpaceAmbientOcclusionSettings, - ScreenSpaceReflectionsUniform, ViewLightProbesUniformOffset, + ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset, ViewScreenSpaceReflectionsUniformOffset, }; use bevy_app::prelude::*; @@ -149,6 +149,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { &'static ViewFogUniformOffset, &'static ViewLightProbesUniformOffset, &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, &'static MeshViewBindGroup, &'static ViewTarget, &'static DeferredLightingIdDepthTexture, @@ -165,6 +166,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { view_fog_offset, view_light_probes_offset, view_ssr_offset, + view_environment_map_offset, mesh_view_bind_group, target, deferred_lighting_id_depth_texture, @@ -220,6 +222,7 @@ impl ViewNode for DeferredOpaquePass3dPbrLightingNode { view_fog_offset.offset, **view_light_probes_offset, **view_ssr_offset, + **view_environment_map_offset, ], ); render_pass.set_bind_group(1, &bind_group_1, &[]); @@ -256,11 +259,11 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 20, + 21, )); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 21, + 22, )); let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index dfe65a0ffe4466..ea84c49f187091 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -57,7 +57,9 @@ pub use prepass::*; pub use render::*; pub use ssao::*; pub use ssr::*; -pub use volumetric_fog::*; +pub use volumetric_fog::{ + FogVolume, FogVolumeBundle, VolumetricFogPlugin, VolumetricFogSettings, VolumetricLight, +}; pub mod prelude { #[doc(hidden)] @@ -288,6 +290,7 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -341,7 +344,7 @@ impl Plugin for PbrPlugin { PostUpdate, ( add_clusters.in_set(SimulationLightSystems::AddClusters), - crate::assign_objects_to_clusters + assign_objects_to_clusters .in_set(SimulationLightSystems::AssignLightsToClusters) .after(TransformSystem::TransformPropagate) .after(VisibilitySystems::CheckVisibility) diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 247aa49f504cc0..d895ac767289c3 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -11,8 +11,7 @@ use bevy_render::{ mesh::Mesh, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, view::{ - InheritedVisibility, RenderLayers, ViewVisibility, VisibilityRange, VisibleEntities, - VisibleEntityRanges, WithMesh, + InheritedVisibility, RenderLayers, ViewVisibility, VisibilityRange, VisibleEntityRanges, }, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -100,7 +99,7 @@ impl Default for PointLightShadowMap { } /// A convenient alias for `Or<(With, With, -/// With)>`, for use with [`VisibleEntities`]. +/// With)>`, for use with [`bevy_render::view::VisibleEntities`]. pub type WithLight = Or<(With, With, With)>; /// Controls the resolution of [`DirectionalLight`] shadow maps. @@ -698,9 +697,7 @@ pub fn check_dir_light_mesh_visibility( match frusta.frusta.get(view) { Some(view_frusta) => { cascade_view_entities.resize(view_frusta.len(), Default::default()); - cascade_view_entities - .iter_mut() - .for_each(VisibleEntities::clear::); + cascade_view_entities.iter_mut().for_each(|x| x.clear()); } None => views_to_remove.push(*view), }; @@ -709,7 +706,7 @@ pub fn check_dir_light_mesh_visibility( visible_entities .entities .entry(*view) - .or_insert_with(|| vec![VisibleEntities::default(); frusta.len()]); + .or_insert_with(|| vec![VisibleMeshEntities::default(); frusta.len()]); } for v in views_to_remove { @@ -790,7 +787,6 @@ pub fn check_dir_light_mesh_visibility( .get_mut(view) .unwrap() .iter_mut() - .map(VisibleEntities::get_mut::) .zip(entities.iter_mut()) .for_each(|(dst, source)| { dst.append(source); @@ -801,7 +797,7 @@ pub fn check_dir_light_mesh_visibility( for (_, cascade_view_entities) in &mut visible_entities.entities { cascade_view_entities .iter_mut() - .map(VisibleEntities::get_mut::) + .map(DerefMut::deref_mut) .for_each(shrink_entities); } } @@ -833,7 +829,7 @@ pub fn check_point_light_mesh_visibility( &SpotLight, &GlobalTransform, &Frustum, - &mut VisibleEntities, + &mut VisibleMeshEntities, Option<&RenderLayers>, )>, mut visible_entity_query: Query< @@ -869,7 +865,7 @@ pub fn check_point_light_mesh_visibility( )) = point_lights.get_mut(light_entity) { for visible_entities in cubemap_visible_entities.iter_mut() { - visible_entities.clear::(); + visible_entities.entities.clear(); } // NOTE: If shadow mapping is disabled for the light then it must have no visible entities @@ -940,13 +936,12 @@ pub fn check_point_light_mesh_visibility( for entities in cubemap_visible_entities_queue.iter_mut() { cubemap_visible_entities .iter_mut() - .map(VisibleEntities::get_mut::) .zip(entities.iter_mut()) - .for_each(|(dst, source)| dst.append(source)); + .for_each(|(dst, source)| dst.entities.append(source)); } for visible_entities in cubemap_visible_entities.iter_mut() { - shrink_entities(visible_entities.get_mut::()); + shrink_entities(visible_entities); } } @@ -954,7 +949,7 @@ pub fn check_point_light_mesh_visibility( if let Ok((point_light, transform, frustum, mut visible_entities, maybe_view_mask)) = spot_lights.get_mut(light_entity) { - visible_entities.clear::(); + visible_entities.clear(); // NOTE: If shadow mapping is disabled for the light then it must have no visible entities if !point_light.shadows_enabled { @@ -1015,10 +1010,10 @@ pub fn check_point_light_mesh_visibility( ); for entities in spot_visible_entities_queue.iter_mut() { - visible_entities.get_mut::().append(entities); + visible_entities.append(entities); } - shrink_entities(visible_entities.get_mut::()); + shrink_entities(visible_entities.deref_mut()); } } } diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 2265287dc83a24..8a78e93083024a 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -50,13 +50,15 @@ use bevy_asset::{AssetId, Handle}; use bevy_ecs::{ bundle::Bundle, component::Component, query::QueryItem, system::lifetimeless::Read, }; +use bevy_math::Quat; use bevy_reflect::Reflect; use bevy_render::{ extract_instances::ExtractInstance, prelude::SpatialBundle, render_asset::RenderAssets, render_resource::{ - binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, + binding_types::{self, uniform_buffer}, + BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages, TextureSampleType, TextureView, }, renderer::RenderDevice, @@ -67,7 +69,8 @@ use std::num::NonZeroU32; use std::ops::Deref; use crate::{ - add_cubemap_texture_view, binding_arrays_are_usable, LightProbe, MAX_VIEW_LIGHT_PROBES, + add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, LightProbe, + MAX_VIEW_LIGHT_PROBES, }; use super::{LightProbeComponent, RenderViewLightProbes}; @@ -96,6 +99,22 @@ pub struct EnvironmentMapLight { /// /// See also . pub intensity: f32, + + /// World space rotation applied to the environment light cubemaps. + /// This is useful for users who require a different axis, such as the Z-axis, to serve + /// as the vertical axis. + pub rotation: Quat, +} + +impl Default for EnvironmentMapLight { + fn default() -> Self { + EnvironmentMapLight { + diffuse_map: Handle::default(), + specular_map: Handle::default(), + intensity: 0.0, + rotation: Quat::IDENTITY, + } + } } /// Like [`EnvironmentMapLight`], but contains asset IDs instead of handles. @@ -193,7 +212,7 @@ impl ExtractInstance for EnvironmentMapIds { /// specular binding arrays respectively, in addition to the sampler. pub(crate) fn get_bind_group_layout_entries( render_device: &RenderDevice, -) -> [BindGroupLayoutEntryBuilder; 3] { +) -> [BindGroupLayoutEntryBuilder; 4] { let mut texture_cube_binding = binding_types::texture_cube(TextureSampleType::Float { filterable: true }); if binding_arrays_are_usable(render_device) { @@ -205,6 +224,7 @@ pub(crate) fn get_bind_group_layout_entries( texture_cube_binding, texture_cube_binding, binding_types::sampler(SamplerBindingType::Filtering), + uniform_buffer::(true).visibility(ShaderStages::FRAGMENT), ] } @@ -312,6 +332,7 @@ impl LightProbeComponent for EnvironmentMapLight { diffuse_map: diffuse_map_handle, specular_map: specular_map_handle, intensity, + .. }) = view_component { if let (Some(_), Some(specular_map)) = ( diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl index 7b9945a8a684c6..459ced93eca035 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -3,6 +3,7 @@ #import bevy_pbr::light_probe::query_light_probe #import bevy_pbr::mesh_view_bindings as bindings #import bevy_pbr::mesh_view_bindings::light_probes +#import bevy_pbr::mesh_view_bindings::environment_map_uniform #import bevy_pbr::lighting::{ F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT } @@ -57,17 +58,29 @@ fn compute_radiances( bindings::specular_environment_maps[query_result.texture_index]) - 1u); if (!found_diffuse_indirect) { + var irradiance_sample_dir = N; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the diffuse environment cubemap itself. + irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + irradiance_sample_dir.z = -irradiance_sample_dir.z; radiances.irradiance = textureSampleLevel( bindings::diffuse_environment_maps[query_result.texture_index], bindings::environment_map_sampler, - vec3(N.xy, -N.z), + irradiance_sample_dir, 0.0).rgb * query_result.intensity; } + var radiance_sample_dir = R; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the specular environment cubemap itself. + radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + radiance_sample_dir.z = -radiance_sample_dir.z; radiances.radiance = textureSampleLevel( bindings::specular_environment_maps[query_result.texture_index], bindings::environment_map_sampler, - vec3(R.xy, -R.z), + radiance_sample_dir, radiance_level).rgb * query_result.intensity; return radiances; @@ -102,17 +115,29 @@ fn compute_radiances( let intensity = light_probes.intensity_for_view; if (!found_diffuse_indirect) { + var irradiance_sample_dir = N; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the diffuse environment cubemap itself. + irradiance_sample_dir = (environment_map_uniform.transform * vec4(irradiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + irradiance_sample_dir.z = -irradiance_sample_dir.z; radiances.irradiance = textureSampleLevel( bindings::diffuse_environment_map, bindings::environment_map_sampler, - vec3(N.xy, -N.z), + irradiance_sample_dir, 0.0).rgb * intensity; } + var radiance_sample_dir = R; + // Rotating the world space ray direction by the environment light map transform matrix, it is + // equivalent to rotating the specular environment cubemap itself. + radiance_sample_dir = (environment_map_uniform.transform * vec4(radiance_sample_dir, 1.0)).xyz; + // Cube maps are left-handed so we negate the z coordinate. + radiance_sample_dir.z = -radiance_sample_dir.z; radiances.radiance = textureSampleLevel( bindings::specular_environment_map, bindings::environment_map_sampler, - vec3(R.xy, -R.z), + radiance_sample_dir, radiance_level).rgb * intensity; return radiances; diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 8c41f28280f7b5..dbcdd680d9fdbe 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -25,7 +25,7 @@ use bevy_render::{ view::ExtractedView, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_transform::prelude::GlobalTransform; +use bevy_transform::{components::Transform, prelude::GlobalTransform}; use bevy_utils::{tracing::error, HashMap}; use std::hash::Hash; @@ -296,6 +296,31 @@ impl LightProbe { } } +/// The uniform struct extracted from [`EnvironmentMapLight`]. +/// Will be available for use in the Environment Map shader. +#[derive(Component, ShaderType, Clone)] +pub struct EnvironmentMapUniform { + /// The world space transformation matrix of the sample ray for environment cubemaps. + transform: Mat4, +} + +impl Default for EnvironmentMapUniform { + fn default() -> Self { + EnvironmentMapUniform { + transform: Mat4::IDENTITY, + } + } +} + +/// A GPU buffer that stores the environment map settings for each view. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct EnvironmentMapUniformBuffer(pub DynamicUniformBuffer); + +/// A component that stores the offset within the +/// [`EnvironmentMapUniformBuffer`] for each view. +#[derive(Component, Default, Deref, DerefMut)] +pub struct ViewEnvironmentMapUniformOffset(u32); + impl Plugin for LightProbePlugin { fn build(&self, app: &mut App) { load_internal_asset!( @@ -330,15 +355,41 @@ impl Plugin for LightProbePlugin { render_app .add_plugins(ExtractInstancesPlugin::::new()) .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, gather_environment_map_uniform) .add_systems(ExtractSchedule, gather_light_probes::) .add_systems(ExtractSchedule, gather_light_probes::) .add_systems( Render, - upload_light_probes.in_set(RenderSet::PrepareResources), + (upload_light_probes, prepare_environment_uniform_buffer) + .in_set(RenderSet::PrepareResources), ); } } +/// Extracts [`EnvironmentMapLight`] from views and creates [`EnvironmentMapUniform`] for them. +/// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance +/// if one does not already exist. +fn gather_environment_map_uniform( + view_query: Extract), With>>, + mut commands: Commands, +) { + for (view_entity, environment_map_light) in view_query.iter() { + let environment_map_uniform = if let Some(environment_map_light) = environment_map_light { + EnvironmentMapUniform { + transform: Transform::from_rotation(environment_map_light.rotation) + .compute_matrix() + .inverse(), + } + } else { + EnvironmentMapUniform::default() + }; + commands + .get_or_spawn(view_entity) + .insert(environment_map_uniform); + } +} + /// Gathers up all light probes of a single type in the scene and assigns them /// to views, performing frustum culling and distance sorting in the process. fn gather_light_probes( @@ -395,6 +446,32 @@ fn gather_light_probes( } } +/// Gathers up environment map settings for each applicable view and +/// writes them into a GPU buffer. +pub fn prepare_environment_uniform_buffer( + mut commands: Commands, + views: Query<(Entity, Option<&EnvironmentMapUniform>), With>, + mut environment_uniform_buffer: ResMut, + render_device: Res, + render_queue: Res, +) { + let Some(mut writer) = + environment_uniform_buffer.get_writer(views.iter().len(), &render_device, &render_queue) + else { + return; + }; + + for (view, environment_uniform) in views.iter() { + let uniform_offset = match environment_uniform { + None => 0, + Some(environment_uniform) => writer.write(environment_uniform), + }; + commands + .entity(view) + .insert(ViewEnvironmentMapUniformOffset(uniform_offset)); + } +} + // A system that runs after [`gather_light_probes`] and populates the GPU // uniforms with the results. // diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index fbb5ea2731379c..913f86a812768a 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -40,7 +40,7 @@ use bevy_ecs::{ }; use bevy_math::{uvec2, vec4, Rect, UVec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::mesh::GpuMesh; +use bevy_render::mesh::RenderMesh; use bevy_render::texture::GpuImage; use bevy_render::{ mesh::Mesh, render_asset::RenderAssets, render_resource::Shader, texture::Image, @@ -145,7 +145,7 @@ fn extract_lightmaps( lightmaps: Extract>, render_mesh_instances: Res, images: Res>, - meshes: Res>, + meshes: Res>, ) { // Clear out the old frame's data. render_lightmaps.render_lightmaps.clear(); diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a1c07e74135013..a000b19a2c6632 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -25,7 +25,7 @@ use bevy_render::{ camera::TemporalJitter, extract_instances::{ExtractInstancesPlugin, ExtractedInstances}, extract_resource::ExtractResource, - mesh::{GpuMesh, MeshVertexBufferLayoutRef}, + mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::*, @@ -456,10 +456,10 @@ impl RenderCommand

for SetMaterial let material_instances = material_instances.into_inner(); let Some(material_asset_id) = material_instances.get(&item.entity()) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(material) = materials.get(*material_asset_id) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_bind_group(I, &material.bind_group, &[]); RenderCommandResult::Success @@ -536,8 +536,7 @@ pub fn queue_material_meshes( material_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, - msaa: Res, - render_meshes: Res>, + render_meshes: Res>, render_materials: Res>>, render_mesh_instances: Res, render_material_instances: Res>, @@ -551,6 +550,7 @@ pub fn queue_material_meshes( Entity, &ExtractedView, &VisibleEntities, + &Msaa, Option<&Tonemapping>, Option<&DebandDither>, Option<&ShadowFilteringMethod>, @@ -576,6 +576,7 @@ pub fn queue_material_meshes( view_entity, view, visible_entities, + msaa, tonemapping, dither, shadow_filter_method, @@ -691,9 +692,14 @@ pub fn queue_material_meshes( continue; }; + let mut mesh_pipeline_key_bits = material.properties.mesh_pipeline_key_bits; + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key( + material.properties.alpha_mode, + msaa, + )); let mut mesh_key = view_key | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) - | material.properties.mesh_pipeline_key_bits; + | mesh_pipeline_key_bits; let lightmap_image = render_lightmaps .render_lightmaps @@ -906,12 +912,11 @@ impl RenderAsset for PreparedMaterial { SRes, SRes>, SRes, - SRes, ); fn prepare_asset( material: Self::SourceAsset, - (render_device, images, fallback_image, pipeline, default_opaque_render_method, msaa): &mut SystemParamItem, + (render_device, images, fallback_image, pipeline, default_opaque_render_method): &mut SystemParamItem, ) -> Result> { match material.as_bind_group( &pipeline.material_layout, @@ -930,7 +935,6 @@ impl RenderAsset for PreparedMaterial { MeshPipelineKey::READS_VIEW_TRANSMISSION_TEXTURE, material.reads_view_transmission_texture(), ); - mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode(), msaa)); Ok(PreparedMaterial { bindings: prepared.bindings, @@ -949,6 +953,7 @@ impl RenderAsset for PreparedMaterial { Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } } diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index 31f6ecd66eb4d7..5701e0f2884491 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -5,13 +5,19 @@ use bevy_asset::{ }; use bevy_math::Vec3; use bevy_reflect::TypePath; +use bevy_tasks::block_on; use bytemuck::{Pod, Zeroable}; use lz4_flex::frame::{FrameDecoder, FrameEncoder}; -use serde::{Deserialize, Serialize}; -use std::{io::Cursor, sync::Arc}; +use std::{ + io::{Read, Write}, + sync::Arc, +}; + +/// Unique identifier for the [`MeshletMesh`] asset format. +const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668; /// The current version of the [`MeshletMesh`] asset format. -pub const MESHLET_MESH_ASSET_VERSION: u64 = 0; +pub const MESHLET_MESH_ASSET_VERSION: u64 = 1; /// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. /// @@ -27,24 +33,24 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 0; /// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes. /// /// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`]. -#[derive(Asset, TypePath, Serialize, Deserialize, Clone)] +#[derive(Asset, TypePath, Clone)] pub struct MeshletMesh { /// The total amount of triangles summed across all LOD 0 meshlets in the mesh. - pub worst_case_meshlet_triangles: u64, + pub(crate) worst_case_meshlet_triangles: u64, /// Raw vertex data bytes for the overall mesh. - pub vertex_data: Arc<[u8]>, + pub(crate) vertex_data: Arc<[u8]>, /// Indices into `vertex_data`. - pub vertex_ids: Arc<[u32]>, + pub(crate) vertex_ids: Arc<[u32]>, /// Indices into `vertex_ids`. - pub indices: Arc<[u8]>, + pub(crate) indices: Arc<[u8]>, /// The list of meshlets making up this mesh. - pub meshlets: Arc<[Meshlet]>, + pub(crate) meshlets: Arc<[Meshlet]>, /// Spherical bounding volumes. - pub bounding_spheres: Arc<[MeshletBoundingSpheres]>, + pub(crate) bounding_spheres: Arc<[MeshletBoundingSpheres]>, } /// A single meshlet within a [`MeshletMesh`]. -#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct Meshlet { /// The offset within the parent mesh's [`MeshletMesh::vertex_ids`] buffer where the indices for this meshlet begin. @@ -56,7 +62,7 @@ pub struct Meshlet { } /// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`]. -#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct MeshletBoundingSpheres { /// The bounding sphere used for frustum and occlusion culling for this meshlet. @@ -68,7 +74,7 @@ pub struct MeshletBoundingSpheres { } /// A spherical bounding volume used for a [`Meshlet`]. -#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct MeshletBoundingSphere { pub center: Vec3, @@ -76,76 +82,154 @@ pub struct MeshletBoundingSphere { } /// An [`AssetLoader`] and [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. -pub struct MeshletMeshSaverLoad; +pub struct MeshletMeshSaverLoader; -impl AssetLoader for MeshletMeshSaverLoad { +impl AssetSaver for MeshletMeshSaverLoader { type Asset = MeshletMesh; type Settings = (); + type OutputLoader = Self; type Error = MeshletMeshSaveOrLoadError; - async fn load<'a>( + async fn save<'a>( &'a self, - reader: &'a mut dyn Reader, - _settings: &'a Self::Settings, - _load_context: &'a mut LoadContext<'_>, - ) -> Result { - let version = read_u64(reader).await?; - if version != MESHLET_MESH_ASSET_VERSION { - return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); - } + writer: &'a mut Writer, + asset: SavedAsset<'a, MeshletMesh>, + _settings: &'a (), + ) -> Result<(), MeshletMeshSaveOrLoadError> { + // Write asset magic number + writer + .write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes()) + .await?; - let mut bytes = Vec::new(); - reader.read_to_end(&mut bytes).await?; - let asset = bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes)))?; + // Write asset version + writer + .write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) + .await?; - Ok(asset) - } + // Compress and write asset data + writer + .write_all(&asset.worst_case_meshlet_triangles.to_le_bytes()) + .await?; + let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer)); + write_slice(&asset.vertex_data, &mut writer)?; + write_slice(&asset.vertex_ids, &mut writer)?; + write_slice(&asset.indices, &mut writer)?; + write_slice(&asset.meshlets, &mut writer)?; + write_slice(&asset.bounding_spheres, &mut writer)?; + writer.finish()?; - fn extensions(&self) -> &[&str] { - &["meshlet_mesh"] + Ok(()) } } -impl AssetSaver for MeshletMeshSaverLoad { +impl AssetLoader for MeshletMeshSaverLoader { type Asset = MeshletMesh; type Settings = (); - type OutputLoader = Self; type Error = MeshletMeshSaveOrLoadError; - async fn save<'a>( + async fn load<'a>( &'a self, - writer: &'a mut Writer, - asset: SavedAsset<'a, Self::Asset>, - _settings: &'a Self::Settings, - ) -> Result<(), Self::Error> { - writer - .write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) - .await?; + reader: &'a mut dyn Reader, + _settings: &'a (), + _load_context: &'a mut LoadContext<'_>, + ) -> Result { + // Load and check magic number + let magic = async_read_u64(reader).await?; + if magic != MESHLET_MESH_ASSET_MAGIC { + return Err(MeshletMeshSaveOrLoadError::WrongFileType); + } - let mut bytes = Vec::new(); - let mut sync_writer = FrameEncoder::new(&mut bytes); - bincode::serialize_into(&mut sync_writer, asset.get())?; - sync_writer.finish()?; - writer.write_all(&bytes).await?; + // Load and check asset version + let version = async_read_u64(reader).await?; + if version != MESHLET_MESH_ASSET_VERSION { + return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); + } - Ok(()) + // Load and decompress asset data + let worst_case_meshlet_triangles = async_read_u64(reader).await?; + let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader)); + let vertex_data = read_slice(reader)?; + let vertex_ids = read_slice(reader)?; + let indices = read_slice(reader)?; + let meshlets = read_slice(reader)?; + let bounding_spheres = read_slice(reader)?; + + Ok(MeshletMesh { + worst_case_meshlet_triangles, + vertex_data, + vertex_ids, + indices, + meshlets, + bounding_spheres, + }) + } + + fn extensions(&self) -> &[&str] { + &["meshlet_mesh"] } } #[derive(thiserror::Error, Debug)] pub enum MeshletMeshSaveOrLoadError { + #[error("file was not a MeshletMesh asset")] + WrongFileType, #[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")] WrongVersion { found: u64 }, - #[error("failed to serialize or deserialize asset data")] - SerializationOrDeserialization(#[from] bincode::Error), #[error("failed to compress or decompress asset data")] CompressionOrDecompression(#[from] lz4_flex::frame::Error), #[error("failed to read or write asset data")] Io(#[from] std::io::Error), } -async fn read_u64(reader: &mut dyn Reader) -> Result { +async fn async_read_u64(reader: &mut dyn Reader) -> Result { let mut bytes = [0u8; 8]; reader.read_exact(&mut bytes).await?; Ok(u64::from_le_bytes(bytes)) } + +fn read_u64(reader: &mut dyn Read) -> Result { + let mut bytes = [0u8; 8]; + reader.read_exact(&mut bytes)?; + Ok(u64::from_le_bytes(bytes)) +} + +fn write_slice( + field: &[T], + writer: &mut dyn Write, +) -> Result<(), MeshletMeshSaveOrLoadError> { + writer.write_all(&(field.len() as u64).to_le_bytes())?; + writer.write_all(bytemuck::cast_slice(field))?; + Ok(()) +} + +fn read_slice(reader: &mut dyn Read) -> Result, std::io::Error> { + let len = read_u64(reader)? as usize; + + let mut data: Arc<[T]> = std::iter::repeat_with(T::zeroed).take(len).collect(); + let slice = Arc::get_mut(&mut data).unwrap(); + reader.read_exact(bytemuck::cast_slice_mut(slice))?; + + Ok(data) +} + +// TODO: Use async for everything and get rid of this adapter +struct AsyncWriteSyncAdapter<'a>(&'a mut Writer); + +impl Write for AsyncWriteSyncAdapter<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + block_on(self.0.write(buf)) + } + + fn flush(&mut self) -> std::io::Result<()> { + block_on(self.0.flush()) + } +} + +// TODO: Use async for everything and get rid of this adapter +struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader); + +impl Read for AsyncReadSyncAdapter<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + block_on(self.0.read(buf)) + } +} diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 882820fd972f3c..f62e00b4340bc0 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -160,7 +160,7 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology); } - if mesh.attributes().map(|(id, _)| id).ne([ + if mesh.attributes().map(|(attribute, _)| attribute.id).ne([ Mesh::ATTRIBUTE_POSITION.id, Mesh::ATTRIBUTE_NORMAL.id, Mesh::ATTRIBUTE_UV_0.id, @@ -294,7 +294,6 @@ fn simplify_meshlet_groups( let target_error = target_error_relative * mesh_scale; // Simplify the group to ~50% triangle count - // TODO: Use simplify_with_locks() let mut error = 0.0; let simplified_group_indices = simplify( &group_indices, diff --git a/crates/bevy_pbr/src/meshlet/gpu_scene.rs b/crates/bevy_pbr/src/meshlet/gpu_scene.rs index f13b44808d9aff..1d4bf7ffe67011 100644 --- a/crates/bevy_pbr/src/meshlet/gpu_scene.rs +++ b/crates/bevy_pbr/src/meshlet/gpu_scene.rs @@ -138,7 +138,7 @@ pub fn extract_meshlet_meshes( gpu_scene .instance_uniforms .get_mut() - .push(MeshUniform::new(&transforms, None)); + .push(MeshUniform::new(&transforms, 0, None)); } } diff --git a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs b/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs index 153eafcc096e1d..e7b71ea25366d1 100644 --- a/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs +++ b/crates/bevy_pbr/src/meshlet/material_draw_nodes.rs @@ -7,8 +7,8 @@ use super::{ MeshletGpuScene, }; use crate::{ - MeshViewBindGroup, PrepassViewBindGroup, ViewFogUniformOffset, ViewLightProbesUniformOffset, - ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, + MeshViewBindGroup, PrepassViewBindGroup, ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, + ViewLightProbesUniformOffset, ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, }; use bevy_core_pipeline::prepass::{ MotionVectorPrepass, PreviousViewUniformOffset, ViewPrepassTextures, @@ -41,6 +41,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { &'static ViewFogUniformOffset, &'static ViewLightProbesUniformOffset, &'static ViewScreenSpaceReflectionsUniformOffset, + &'static ViewEnvironmentMapUniformOffset, &'static MeshletViewMaterialsMainOpaquePass, &'static MeshletViewBindGroups, &'static MeshletViewResources, @@ -59,6 +60,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { view_fog_offset, view_light_probes_offset, view_ssr_offset, + view_environment_map_offset, meshlet_view_materials, meshlet_view_bind_groups, meshlet_view_resources, @@ -111,6 +113,7 @@ impl ViewNode for MeshletMainOpaquePass3dNode { view_fog_offset.offset, **view_light_probes_offset, **view_ssr_offset, + **view_environment_map_offset, ], ); render_pass.set_bind_group(1, meshlet_material_draw_bind_group, &[]); diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 3d00683bef771b..a55dc42247a805 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -30,7 +30,7 @@ pub(crate) use self::{ }, }; -pub use self::asset::*; +pub use self::asset::{MeshletMesh, MeshletMeshSaverLoader}; #[cfg(feature = "meshlet_processor")] pub use self::from_mesh::MeshToMeshletMeshConversionError; @@ -105,9 +105,10 @@ const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = /// * Requires preprocessing meshes. See [`MeshletMesh`] for details. /// * Limitations on the kinds of materials you can use. See [`MeshletMesh`] for details. /// -/// This plugin is not compatible with [`Msaa`], and adding this plugin will disable it. +/// This plugin is not compatible with [`Msaa`]. Any camera rendering a [`MeshletMesh`] must have +/// [`Msaa`] set to [`Msaa::Off`]. /// -/// This plugin does not work on WASM. +/// This plugin does not work on Wasm. /// /// Mixing forward+prepass and deferred rendering for opaque materials is not currently supported when using this plugin. /// You must use one or the other by setting [`crate::DefaultOpaqueRendererMethod`]. @@ -118,6 +119,9 @@ pub struct MeshletPlugin; impl Plugin for MeshletPlugin { fn build(&self, app: &mut App) { + #[cfg(target_endian = "big")] + compile_error!("MeshletPlugin is only supported on little-endian processors."); + load_internal_asset!( app, MESHLET_BINDINGS_SHADER_HANDLE, @@ -168,8 +172,7 @@ impl Plugin for MeshletPlugin { ); app.init_asset::() - .register_asset_loader(MeshletMeshSaverLoad) - .insert_resource(Msaa::Off) + .register_asset_loader(MeshletMeshSaverLoader) .add_systems( PostUpdate, check_visibility::.in_set(VisibilitySystems::CheckVisibility), @@ -279,15 +282,20 @@ fn configure_meshlet_views( mut views_3d: Query<( Entity, &mut Camera3d, + &Msaa, Has, Has, Has, )>, mut commands: Commands, ) { - for (entity, mut camera_3d, normal_prepass, motion_vector_prepass, deferred_prepass) in + for (entity, mut camera_3d, msaa, normal_prepass, motion_vector_prepass, deferred_prepass) in &mut views_3d { + if *msaa != Msaa::Off { + panic!("MeshletPlugin can't be used. MSAA is not supported."); + } + let mut usages: TextureUsages = camera_3d.depth_texture_usages.into(); usages |= TextureUsages::TEXTURE_BINDING; camera_3d.depth_texture_usages = usages.into(); diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 5330e055ee3c36..a8ca69a41f176d 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -1,6 +1,6 @@ mod prepass_bindings; -use bevy_render::mesh::{GpuMesh, MeshVertexBufferLayoutRef}; +use bevy_render::mesh::{MeshVertexBufferLayoutRef, RenderMesh}; use bevy_render::render_resource::binding_types::uniform_buffer; use bevy_render::view::WithMesh; pub use prepass_bindings::*; @@ -679,8 +679,7 @@ pub fn queue_prepass_material_meshes( prepass_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, - msaa: Res, - render_meshes: Res>, + render_meshes: Res>, render_mesh_instances: Res, render_materials: Res>>, render_material_instances: Res>, @@ -693,6 +692,7 @@ pub fn queue_prepass_material_meshes( ( Entity, &VisibleEntities, + &Msaa, Option<&DepthPrepass>, Option<&NormalPrepass>, Option<&MotionVectorPrepass>, @@ -722,6 +722,7 @@ pub fn queue_prepass_material_meshes( for ( view, visible_entities, + msaa, depth_prepass, normal_prepass, motion_vector_prepass, @@ -780,7 +781,7 @@ pub fn queue_prepass_material_meshes( let alpha_mode = material.properties.alpha_mode; match alpha_mode { AlphaMode::Opaque | AlphaMode::AlphaToCoverage | AlphaMode::Mask(_) => { - mesh_key |= alpha_mode_pipeline_key(alpha_mode, &msaa); + mesh_key |= alpha_mode_pipeline_key(alpha_mode, msaa); } AlphaMode::Blend | AlphaMode::Premultiplied @@ -851,6 +852,9 @@ pub fn queue_prepass_material_meshes( } }; + mesh_instance + .material_bind_group_id + .set(material.get_bind_group_id()); match mesh_key .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) { diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index cb1328fae1ebf8..7a0fcb89ed8f64 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -1,5 +1,6 @@ #import bevy_pbr::{ prepass_bindings, + mesh_bindings::mesh, mesh_functions, prepass_io::{Vertex, VertexOutput, FragmentOutput}, skinning, @@ -15,18 +16,21 @@ #ifdef MORPH_TARGETS fn morph_vertex(vertex_in: Vertex) -> Vertex { var vertex = vertex_in; + let first_vertex = mesh[vertex.instance_index].first_vertex_index; + let vertex_index = vertex.index - first_vertex; + let weight_count = morph::layer_count(); for (var i: u32 = 0u; i < weight_count; i ++) { let weight = morph::weight_at(i); if weight == 0.0 { continue; } - vertex.position += weight * morph::morph(vertex.index, morph::position_offset, i); + vertex.position += weight * morph::morph(vertex_index, morph::position_offset, i); #ifdef VERTEX_NORMALS - vertex.normal += weight * morph::morph(vertex.index, morph::normal_offset, i); + vertex.normal += weight * morph::morph(vertex_index, morph::normal_offset, i); #endif #ifdef VERTEX_TANGENTS - vertex.tangent += vec4(weight * morph::morph(vertex.index, morph::tangent_offset, i), 0.0); + vertex.tangent += vec4(weight * morph::morph(vertex_index, morph::tangent_offset, i), 0.0); #endif } return vertex; diff --git a/crates/bevy_pbr/src/render/clustered_forward.wgsl b/crates/bevy_pbr/src/render/clustered_forward.wgsl index 5da176ab19cd8a..79b50276b2aa89 100644 --- a/crates/bevy_pbr/src/render/clustered_forward.wgsl +++ b/crates/bevy_pbr/src/render/clustered_forward.wgsl @@ -84,11 +84,12 @@ fn cluster_debug_visualization( if (z_slice & 1u) == 1u { z_slice = z_slice + bindings::lights.cluster_dimensions.z / 2u; } - let slice_color = hsv_to_rgb( + let slice_color_hsv = vec3( f32(z_slice) / f32(bindings::lights.cluster_dimensions.z + 1u) * PI_2, 1.0, 0.5 ); + let slice_color = hsv_to_rgb(slice_color_hsv); output_color = vec4( (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color, output_color.a @@ -115,7 +116,8 @@ fn cluster_debug_visualization( // NOTE: Visualizes the cluster to which the fragment belongs let cluster_overlay_alpha = 0.1; var rng = cluster_index; - let cluster_color = hsv_to_rgb(rand_f(&rng) * PI_2, 1.0, 0.5); + let cluster_color_hsv = vec3(rand_f(&rng) * PI_2, 1.0, 0.5); + let cluster_color = hsv_to_rgb(cluster_color_hsv); output_color = vec4( (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * cluster_color, output_color.a diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 1356e94825ccc8..ae509c8739c3cc 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,13 +1,13 @@ use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; -use bevy_core_pipeline::core_3d::CORE_3D_DEPTH_FORMAT; +use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; use bevy_ecs::entity::EntityHashSet; use bevy_ecs::prelude::*; use bevy_ecs::{entity::EntityHashMap, system::lifetimeless::Read}; use bevy_math::{Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render::{ diagnostic::RecordDiagnostics, - mesh::GpuMesh, + mesh::RenderMesh, primitives::{CascadesFrusta, CubemapFrusta, Frustum, HalfSpace}, render_asset::RenderAssets, render_graph::{Node, NodeRunError, RenderGraphContext}, @@ -15,7 +15,7 @@ use bevy_render::{ render_resource::*, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::*, - view::{ExtractedView, RenderLayers, ViewVisibility, VisibleEntities, WithMesh}, + view::{ExtractedView, RenderLayers, ViewVisibility}, Extract, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; @@ -186,7 +186,7 @@ pub fn extract_lights( spot_lights: Extract< Query<( &SpotLight, - &VisibleEntities, + &VisibleMeshEntities, &GlobalTransform, &ViewVisibility, &Frustum, @@ -515,12 +515,15 @@ pub fn prepare_lights( render_queue: Res, mut global_light_meta: ResMut, mut light_meta: ResMut, - views: Query<( - Entity, - &ExtractedView, - &ExtractedClusterConfig, - Option<&RenderLayers>, - )>, + views: Query< + ( + Entity, + &ExtractedView, + &ExtractedClusterConfig, + Option<&RenderLayers>, + ), + With, + >, ambient_light: Res, point_light_shadow_map: Res, directional_light_shadow_map: Res, @@ -634,7 +637,7 @@ pub fn prepare_lights( // point light shadows and `spot_light_shadow_maps_count` spot light shadow maps, // - then by entity as a stable key to ensure that a consistent set of lights are chosen if the light count limit is exceeded. point_lights.sort_by(|(entity_1, light_1, _), (entity_2, light_2, _)| { - crate::cluster::clusterable_object_order( + clusterable_object_order( ( entity_1, &light_1.shadows_enabled, @@ -1162,7 +1165,7 @@ pub fn prepare_lights( pub fn queue_shadows( shadow_draw_functions: Res>, prepass_pipeline: Res>, - render_meshes: Res>, + render_meshes: Res>, render_mesh_instances: Res, render_materials: Res>>, render_material_instances: Res>, @@ -1174,7 +1177,7 @@ pub fn queue_shadows( mut view_light_entities: Query<&LightEntity>, point_light_entities: Query<&CubemapVisibleEntities, With>, directional_light_entities: Query<&CascadesVisibleEntities, With>, - spot_light_entities: Query<&VisibleEntities, With>, + spot_light_entities: Query<&VisibleMeshEntities, With>, ) where M::Data: PartialEq + Eq + Hash + Clone, { @@ -1218,7 +1221,7 @@ pub fn queue_shadows( // NOTE: Lights with shadow mapping disabled will have no visible entities // so no meshes will be queued - for entity in visible_entities.iter::().copied() { + for entity in visible_entities.iter().copied() { let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(entity) else { continue; @@ -1445,7 +1448,11 @@ impl Node for ShadowPassNode { let pass_span = diagnostics.pass_span(&mut render_pass, view_light.pass_name.clone()); - shadow_phase.render(&mut render_pass, world, view_light_entity); + if let Err(err) = + shadow_phase.render(&mut render_pass, world, view_light_entity) + { + error!("Error encountered while rendering the shadow phase {err:?}"); + } pass_span.end(&mut render_pass); drop(render_pass); diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 8432cbd97d7499..a7bbb7515aa1ad 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1,5 +1,6 @@ -use std::mem; +use std::mem::{self, size_of}; +use allocator::MeshAllocator; use bevy_asset::{load_internal_asset, AssetId}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, @@ -185,8 +186,8 @@ impl Plugin for MeshRenderPlugin { if use_gpu_instance_buffer_builder { render_app - .init_resource::>( - ) + .init_resource::>() + .init_resource::() .add_systems( ExtractSchedule, extract_meshes_for_gpu_building.in_set(ExtractMeshesSet), @@ -199,6 +200,9 @@ impl Plugin for MeshRenderPlugin { gpu_preprocessing::delete_old_work_item_buffers:: .in_set(RenderSet::ManageViews) .after(prepare_view_targets), + collect_meshes_for_gpu_building + .in_set(RenderSet::PrepareAssets) + .after(allocator::allocate_and_free_meshes), ), ); } else { @@ -274,6 +278,18 @@ pub struct MeshUniform { // // (MSB: most significant bit; LSB: least significant bit.) pub lightmap_uv_rect: UVec2, + /// The index of this mesh's first vertex in the vertex buffer. + /// + /// Multiple meshes can be packed into a single vertex buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first vertex in + /// this mesh in that buffer. + pub first_vertex_index: u32, + /// Padding. + pub pad_a: u32, + /// Padding. + pub pad_b: u32, + /// Padding. + pub pad_c: u32, } /// Information that has to be transferred from CPU to GPU in order to produce @@ -304,6 +320,18 @@ pub struct MeshInputUniform { /// /// This is used for TAA. If not present, this will be `u32::MAX`. pub previous_input_index: u32, + /// The index of this mesh's first vertex in the vertex buffer. + /// + /// Multiple meshes can be packed into a single vertex buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first vertex in + /// this mesh in that buffer. + pub first_vertex_index: u32, + /// Padding. + pub pad_a: u32, + /// Padding. + pub pad_b: u32, + /// Padding. + pub pad_c: u32, } /// Information about each mesh instance needed to cull it on GPU. @@ -330,7 +358,11 @@ pub struct MeshCullingData { pub struct MeshCullingDataBuffer(RawBufferVec); impl MeshUniform { - pub fn new(mesh_transforms: &MeshTransforms, maybe_lightmap_uv_rect: Option) -> Self { + pub fn new( + mesh_transforms: &MeshTransforms, + first_vertex_index: u32, + maybe_lightmap_uv_rect: Option, + ) -> Self { let (local_from_world_transpose_a, local_from_world_transpose_b) = mesh_transforms.world_from_local.inverse_transpose_3x3(); Self { @@ -340,6 +372,10 @@ impl MeshUniform { local_from_world_transpose_a, local_from_world_transpose_b, flags: mesh_transforms.flags, + first_vertex_index, + pad_a: 0, + pad_b: 0, + pad_c: 0, } } } @@ -514,6 +550,14 @@ pub enum RenderMeshInstanceGpuQueue { GpuCulling(Vec<(Entity, RenderMeshInstanceGpuBuilder, MeshCullingData)>), } +/// The per-thread queues containing mesh instances, populated during the +/// extract phase. +/// +/// These are filled in [`extract_meshes_for_gpu_building`] and consumed in +/// [`collect_meshes_for_gpu_building`]. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct RenderMeshInstanceGpuQueues(Parallel); + impl RenderMeshInstanceShared { fn from_components( previous_transform: Option<&PreviousGlobalTransform>, @@ -718,7 +762,14 @@ impl RenderMeshInstanceGpuBuilder { entity: Entity, render_mesh_instances: &mut EntityHashMap, current_input_buffer: &mut RawBufferVec, + mesh_allocator: &MeshAllocator, ) -> usize { + let first_vertex_index = match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) + { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; + // Push the mesh input uniform. let current_uniform_index = current_input_buffer.push(MeshInputUniform { world_from_local: self.world_from_local.to_transpose(), @@ -728,6 +779,10 @@ impl RenderMeshInstanceGpuBuilder { Some(previous_input_index) => previous_input_index.into(), None => u32::MAX, }, + first_vertex_index, + pad_a: 0, + pad_b: 0, + pad_c: 0, }); // Record the [`RenderMeshInstance`]. @@ -899,11 +954,7 @@ pub fn extract_meshes_for_cpu_building( pub fn extract_meshes_for_gpu_building( mut render_mesh_instances: ResMut, render_visibility_ranges: Res, - mut batched_instance_buffers: ResMut< - gpu_preprocessing::BatchedInstanceBuffers, - >, - mut mesh_culling_data_buffer: ResMut, - mut render_mesh_instance_queues: Local>, + mut render_mesh_instance_queues: ResMut, meshes_query: Extract< Query<( Entity, @@ -976,8 +1027,7 @@ pub fn extract_meshes_for_gpu_building( no_automatic_batching, ); - let lightmap_uv_rect = - lightmap::pack_lightmap_uv_rect(lightmap.map(|lightmap| lightmap.uv_rect)); + let lightmap_uv_rect = pack_lightmap_uv_rect(lightmap.map(|lightmap| lightmap.uv_rect)); let gpu_mesh_culling_data = any_gpu_culling.then(|| MeshCullingData::new(aabb)); @@ -1003,13 +1053,6 @@ pub fn extract_meshes_for_gpu_building( queue.push(entity, gpu_mesh_instance_builder, gpu_mesh_culling_data); }, ); - - collect_meshes_for_gpu_building( - render_mesh_instances, - &mut batched_instance_buffers, - &mut mesh_culling_data_buffer, - &mut render_mesh_instance_queues, - ); } /// A system that sets the [`RenderMeshInstanceFlags`] for each mesh based on @@ -1043,22 +1086,28 @@ fn set_mesh_motion_vector_flags( /// Creates the [`RenderMeshInstanceGpu`]s and [`MeshInputUniform`]s when GPU /// mesh uniforms are built. -fn collect_meshes_for_gpu_building( - render_mesh_instances: &mut RenderMeshInstancesGpu, - batched_instance_buffers: &mut gpu_preprocessing::BatchedInstanceBuffers< - MeshUniform, - MeshInputUniform, +pub fn collect_meshes_for_gpu_building( + render_mesh_instances: ResMut, + batched_instance_buffers: ResMut< + gpu_preprocessing::BatchedInstanceBuffers, >, - mesh_culling_data_buffer: &mut MeshCullingDataBuffer, - render_mesh_instance_queues: &mut Parallel, + mut mesh_culling_data_buffer: ResMut, + mut render_mesh_instance_queues: ResMut, + mesh_allocator: Res, ) { + let RenderMeshInstances::GpuBuilding(ref mut render_mesh_instances) = + render_mesh_instances.into_inner() + else { + return; + }; + // Collect render mesh instances. Build up the uniform buffer. let gpu_preprocessing::BatchedInstanceBuffers { ref mut current_input_buffer, ref mut previous_input_buffer, .. - } = batched_instance_buffers; + } = batched_instance_buffers.into_inner(); // Swap buffers. mem::swap(current_input_buffer, previous_input_buffer); @@ -1075,8 +1124,9 @@ fn collect_meshes_for_gpu_building( for (entity, mesh_instance_builder) in queue.drain(..) { mesh_instance_builder.add_to( entity, - render_mesh_instances, + &mut *render_mesh_instances, current_input_buffer, + &mesh_allocator, ); } } @@ -1084,10 +1134,12 @@ fn collect_meshes_for_gpu_building( for (entity, mesh_instance_builder, mesh_culling_builder) in queue.drain(..) { let instance_data_index = mesh_instance_builder.add_to( entity, - render_mesh_instances, + &mut *render_mesh_instances, current_input_buffer, + &mesh_allocator, ); - let culling_data_index = mesh_culling_builder.add_to(mesh_culling_data_buffer); + let culling_data_index = + mesh_culling_builder.add_to(&mut mesh_culling_data_buffer); debug_assert_eq!(instance_data_index, culling_data_index); } } @@ -1209,7 +1261,8 @@ impl GetBatchData for MeshPipeline { type Param = ( SRes, SRes, - SRes>, + SRes>, + SRes, ); // The material bind group ID, the mesh ID, and the lightmap ID, // respectively. @@ -1218,7 +1271,7 @@ impl GetBatchData for MeshPipeline { type BufferData = MeshUniform; fn get_batch_data( - (mesh_instances, lightmaps, _): &SystemParamItem, + (mesh_instances, lightmaps, _, mesh_allocator): &SystemParamItem, entity: Entity, ) -> Option<(Self::BufferData, Option)> { let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { @@ -1229,11 +1282,17 @@ impl GetBatchData for MeshPipeline { return None; }; let mesh_instance = mesh_instances.get(&entity)?; + let first_vertex_index = + match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; let maybe_lightmap = lightmaps.render_lightmaps.get(&entity); Some(( MeshUniform::new( &mesh_instance.transforms, + first_vertex_index, maybe_lightmap.map(|lightmap| lightmap.uv_rect), ), mesh_instance.should_batch().then_some(( @@ -1249,7 +1308,7 @@ impl GetFullBatchData for MeshPipeline { type BufferInputData = MeshInputUniform; fn get_index_and_compare_data( - (mesh_instances, lightmaps, _): &SystemParamItem, + (mesh_instances, lightmaps, _, _): &SystemParamItem, entity: Entity, ) -> Option<(NonMaxU32, Option)> { // This should only be called during GPU building. @@ -1275,7 +1334,7 @@ impl GetFullBatchData for MeshPipeline { } fn get_binned_batch_data( - (mesh_instances, lightmaps, _): &SystemParamItem, + (mesh_instances, lightmaps, _, mesh_allocator): &SystemParamItem, entity: Entity, ) -> Option { let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { @@ -1285,16 +1344,22 @@ impl GetFullBatchData for MeshPipeline { return None; }; let mesh_instance = mesh_instances.get(&entity)?; + let first_vertex_index = + match mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) { + Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, + None => 0, + }; let maybe_lightmap = lightmaps.render_lightmaps.get(&entity); Some(MeshUniform::new( &mesh_instance.transforms, + first_vertex_index, maybe_lightmap.map(|lightmap| lightmap.uv_rect), )) } fn get_binned_index( - (mesh_instances, _, _): &SystemParamItem, + (mesh_instances, _, _, _): &SystemParamItem, entity: Entity, ) -> Option { // This should only be called during GPU building. @@ -1312,7 +1377,7 @@ impl GetFullBatchData for MeshPipeline { } fn get_batch_indirect_parameters_index( - (mesh_instances, _, meshes): &SystemParamItem, + (mesh_instances, _, meshes, mesh_allocator): &SystemParamItem, indirect_parameters_buffer: &mut IndirectParametersBuffer, entity: Entity, instance_index: u32, @@ -1320,6 +1385,7 @@ impl GetFullBatchData for MeshPipeline { get_batch_indirect_parameters_index( mesh_instances, meshes, + mesh_allocator, indirect_parameters_buffer, entity, instance_index, @@ -1332,7 +1398,8 @@ impl GetFullBatchData for MeshPipeline { /// parameters. fn get_batch_indirect_parameters_index( mesh_instances: &RenderMeshInstances, - meshes: &RenderAssets, + meshes: &RenderAssets, + mesh_allocator: &MeshAllocator, indirect_parameters_buffer: &mut IndirectParametersBuffer, entity: Entity, instance_index: u32, @@ -1348,24 +1415,29 @@ fn get_batch_indirect_parameters_index( let mesh_instance = mesh_instances.get(&entity)?; let mesh = meshes.get(mesh_instance.mesh_asset_id)?; + let vertex_buffer_slice = mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id)?; // Note that `IndirectParameters` covers both of these structures, even // though they actually have distinct layouts. See the comment above that // type for more information. let indirect_parameters = match mesh.buffer_info { - GpuBufferInfo::Indexed { + RenderMeshBufferInfo::Indexed { count: index_count, .. - } => IndirectParameters { - vertex_or_index_count: index_count, - instance_count: 0, - first_vertex: 0, - base_vertex_or_first_instance: 0, - first_instance: instance_index, - }, - GpuBufferInfo::NonIndexed => IndirectParameters { + } => { + let index_buffer_slice = + mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id)?; + IndirectParameters { + vertex_or_index_count: index_count, + instance_count: 0, + first_vertex_or_first_index: index_buffer_slice.range.start, + base_vertex_or_first_instance: vertex_buffer_slice.range.start, + first_instance: instance_index, + } + } + RenderMeshBufferInfo::NonIndexed => IndirectParameters { vertex_or_index_count: mesh.vertex_count, instance_count: 0, - first_vertex: 0, + first_vertex_or_first_index: vertex_buffer_slice.range.start, base_vertex_or_first_instance: instance_index, first_instance: instance_index, }, @@ -1765,11 +1837,11 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 20, + 21, )); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 21, + 22, )); let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); @@ -1945,7 +2017,7 @@ impl MeshBindGroups { self.morph_targets.clear(); self.lightmaps.clear(); } - /// Get the `BindGroup` for `GpuMesh` with given `handle_id` and lightmap + /// Get the `BindGroup` for `RenderMesh` with given `handle_id` and lightmap /// key `lightmap`. pub fn get( &self, @@ -1982,7 +2054,7 @@ impl MeshBindGroupPair { #[allow(clippy::too_many_arguments)] pub fn prepare_mesh_bind_group( - meshes: Res>, + meshes: Res>, images: Res>, mut groups: ResMut, mesh_pipeline: Res, @@ -2096,6 +2168,7 @@ impl RenderCommand

for SetMeshViewBindGroup Read, Read, Read, + Read, Read, ); type ItemQuery = (); @@ -2103,10 +2176,15 @@ impl RenderCommand

for SetMeshViewBindGroup #[inline] fn render<'w>( _item: &P, - (view_uniform, view_lights, view_fog, view_light_probes, view_ssr, mesh_view_bind_group): ROQueryItem< - 'w, - Self::ViewQuery, - >, + ( + view_uniform, + view_lights, + view_fog, + view_light_probes, + view_ssr, + view_environment_map, + mesh_view_bind_group, + ): ROQueryItem<'w, Self::ViewQuery>, _entity: Option<()>, _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -2120,6 +2198,7 @@ impl RenderCommand

for SetMeshViewBindGroup view_fog.offset, **view_light_probes, **view_ssr, + **view_environment_map, ], ); @@ -2181,12 +2260,11 @@ impl RenderCommand

for SetMeshBindGroup { is_morphed, has_motion_vector_prepass, ) else { - error!( + return RenderCommandResult::Failure( "The MeshBindGroups resource wasn't set in the render phase. \ It should be set by the prepare_mesh_bind_group system.\n\ - This is a bevy bug! Please open an issue." + This is a bevy bug! Please open an issue.", ); - return RenderCommandResult::Failure; }; let mut dynamic_offsets: [u32; 3] = Default::default(); @@ -2238,10 +2316,11 @@ impl RenderCommand

for SetMeshBindGroup { pub struct DrawMesh; impl RenderCommand

for DrawMesh { type Param = ( - SRes>, + SRes>, SRes, SRes, SRes, + SRes, Option>, ); type ViewQuery = Has; @@ -2251,7 +2330,14 @@ impl RenderCommand

for DrawMesh { item: &P, has_preprocess_bind_group: ROQueryItem, _item_query: Option<()>, - (meshes, mesh_instances, indirect_parameters_buffer, pipeline_cache, preprocess_pipelines): SystemParamItem<'w, '_, Self::Param>, + ( + meshes, + mesh_instances, + indirect_parameters_buffer, + pipeline_cache, + mesh_allocator, + preprocess_pipelines, + ): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { // If we're using GPU preprocessing, then we're dependent on that @@ -2261,19 +2347,23 @@ impl RenderCommand

for DrawMesh { if !has_preprocess_bind_group || !preprocess_pipelines.pipelines_are_loaded(&pipeline_cache) { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; } } let meshes = meshes.into_inner(); let mesh_instances = mesh_instances.into_inner(); let indirect_parameters_buffer = indirect_parameters_buffer.into_inner(); + let mesh_allocator = mesh_allocator.into_inner(); let Some(mesh_asset_id) = mesh_instances.mesh_asset_id(item.entity()) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(gpu_mesh) = meshes.get(mesh_asset_id) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; + }; + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_asset_id) else { + return RenderCommandResult::Skip; }; // Calculate the indirect offset, and look up the buffer. @@ -2282,30 +2372,40 @@ impl RenderCommand

for DrawMesh { Some(index) => match indirect_parameters_buffer.buffer() { None => { warn!("Not rendering mesh because indirect parameters buffer wasn't present"); - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; } Some(buffer) => Some(( - index as u64 * mem::size_of::() as u64, + index as u64 * size_of::() as u64, buffer, )), }, }; - pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); let batch_range = item.batch_range(); // Draw either directly or indirectly, as appropriate. match &gpu_mesh.buffer_info { - GpuBufferInfo::Indexed { - buffer, + RenderMeshBufferInfo::Indexed { index_format, count, } => { - pass.set_index_buffer(buffer.slice(..), 0, *index_format); + let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(&mesh_asset_id) + else { + return RenderCommandResult::Skip; + }; + + pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); + match indirect_parameters { None => { - pass.draw_indexed(0..*count, 0, batch_range.clone()); + pass.draw_indexed( + index_buffer_slice.range.start + ..(index_buffer_slice.range.start + *count), + vertex_buffer_slice.range.start as i32, + batch_range.clone(), + ); } Some((indirect_parameters_offset, indirect_parameters_buffer)) => pass .draw_indexed_indirect( @@ -2314,9 +2414,9 @@ impl RenderCommand

for DrawMesh { ), } } - GpuBufferInfo::NonIndexed => match indirect_parameters { + RenderMeshBufferInfo::NonIndexed => match indirect_parameters { None => { - pass.draw(0..gpu_mesh.vertex_count, batch_range.clone()); + pass.draw(vertex_buffer_slice.range, batch_range.clone()); } Some((indirect_parameters_offset, indirect_parameters_buffer)) => { pass.draw_indirect(indirect_parameters_buffer, indirect_parameters_offset); diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index f4386d97ed27be..7d617755adc554 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -1,4 +1,5 @@ #import bevy_pbr::{ + mesh_bindings::mesh, mesh_functions, skinning, morph::morph, @@ -9,18 +10,21 @@ #ifdef MORPH_TARGETS fn morph_vertex(vertex_in: Vertex) -> Vertex { var vertex = vertex_in; + let first_vertex = mesh[vertex.instance_index].first_vertex_index; + let vertex_index = vertex.index - first_vertex; + let weight_count = bevy_pbr::morph::layer_count(); for (var i: u32 = 0u; i < weight_count; i ++) { let weight = bevy_pbr::morph::weight_at(i); if weight == 0.0 { continue; } - vertex.position += weight * morph(vertex.index, bevy_pbr::morph::position_offset, i); + vertex.position += weight * morph(vertex_index, bevy_pbr::morph::position_offset, i); #ifdef VERTEX_NORMALS - vertex.normal += weight * morph(vertex.index, bevy_pbr::morph::normal_offset, i); + vertex.normal += weight * morph(vertex_index, bevy_pbr::morph::normal_offset, i); #endif #ifdef VERTEX_TANGENTS - vertex.tangent += vec4(weight * morph(vertex.index, bevy_pbr::morph::tangent_offset, i), 0.0); + vertex.tangent += vec4(weight * morph(vertex_index, bevy_pbr::morph::tangent_offset, i), 0.0); #endif } return vertex; diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index 0ba7eb5de28b2e..4891588552fbfd 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -4,13 +4,14 @@ use bevy_math::Mat4; use bevy_render::{ mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice, texture::GpuImage, }; +use std::mem::size_of; use crate::render::skin::MAX_JOINTS; -const MORPH_WEIGHT_SIZE: usize = std::mem::size_of::(); +const MORPH_WEIGHT_SIZE: usize = size_of::(); pub const MORPH_BUFFER_SIZE: usize = MAX_MORPH_WEIGHTS * MORPH_WEIGHT_SIZE; -const JOINT_SIZE: usize = std::mem::size_of::(); +const JOINT_SIZE: usize = size_of::(); pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; /// Individual layout entries. diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 02d8042d5f9548..6a5a1fcf06e338 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -22,6 +22,10 @@ struct MeshInput { // The index of this mesh's `MeshInput` in the `previous_input` array, if // applicable. If not present, this is `u32::MAX`. previous_input_index: u32, + first_vertex_index: u32, + pad_a: u32, + pad_b: u32, + pad_c: u32, } // Information about each mesh instance needed to cull it on GPU. @@ -186,4 +190,5 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { output[mesh_output_index].local_from_world_transpose_b = local_from_world_transpose_b; output[mesh_output_index].flags = current_input[input_index].flags; output[mesh_output_index].lightmap_uv_rect = current_input[input_index].lightmap_uv_rect; + output[mesh_output_index].first_vertex_index = current_input[input_index].first_vertex_index; } diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 7b45bac0ca95c3..57576a3bb3805d 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -15,6 +15,11 @@ struct Mesh { // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, lightmap_uv_rect: vec2, + // The index of the mesh's first vertex in the vertex buffer. + first_vertex_index: u32, + pad_a: u32, + pad_b: u32, + pad_c: u32, }; #ifdef SKINNED diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 465fac80118966..36c5d7bfe044f7 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -41,11 +41,11 @@ use crate::{ self, IrradianceVolume, RenderViewIrradianceVolumeBindGroupEntries, IRRADIANCE_VOLUMES_ARE_USABLE, }, - prepass, FogMeta, GlobalClusterableObjectMeta, GpuClusterableObjects, GpuFog, GpuLights, - LightMeta, LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, - RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, ScreenSpaceReflectionsBuffer, - ScreenSpaceReflectionsUniform, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, - CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, + prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, + GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, + MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, + ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, + ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; #[derive(Clone)] @@ -299,6 +299,7 @@ fn layout_entries( (15, environment_map_entries[0]), (16, environment_map_entries[1]), (17, environment_map_entries[2]), + (18, environment_map_entries[3]), )); // Irradiance volumes @@ -306,16 +307,16 @@ fn layout_entries( let irradiance_volume_entries = irradiance_volume::get_bind_group_layout_entries(render_device); entries = entries.extend_with_indices(( - (18, irradiance_volume_entries[0]), - (19, irradiance_volume_entries[1]), + (19, irradiance_volume_entries[0]), + (20, irradiance_volume_entries[1]), )); } // Tonemapping let tonemapping_lut_entries = get_lut_bind_group_layout_entries(); entries = entries.extend_with_indices(( - (20, tonemapping_lut_entries[0]), - (21, tonemapping_lut_entries[1]), + (21, tonemapping_lut_entries[0]), + (22, tonemapping_lut_entries[1]), )); // Prepass @@ -325,7 +326,7 @@ fn layout_entries( { for (entry, binding) in prepass::get_bind_group_layout_entries(layout_key) .iter() - .zip([22, 23, 24, 25]) + .zip([23, 24, 25, 26]) { if let Some(entry) = entry { entries = entries.extend_with_indices(((binding as u32, *entry),)); @@ -336,10 +337,10 @@ fn layout_entries( // View Transmission Texture entries = entries.extend_with_indices(( ( - 26, + 27, texture_2d(TextureSampleType::Float { filterable: true }), ), - (27, sampler(SamplerBindingType::Filtering)), + (28, sampler(SamplerBindingType::Filtering)), )); entries.to_vec() @@ -450,11 +451,12 @@ pub fn prepare_mesh_view_bind_groups( light_meta: Res, global_light_meta: Res, fog_meta: Res, - view_uniforms: Res, + (view_uniforms, environment_map_uniform): (Res, Res), views: Query<( Entity, &ViewShadowBindings, &ViewClusterBindings, + &Msaa, Option<&ScreenSpaceAmbientOcclusionTextures>, Option<&ViewPrepassTextures>, Option<&ViewTransmissionTexture>, @@ -468,7 +470,6 @@ pub fn prepare_mesh_view_bind_groups( Res, Res, ), - msaa: Res, globals_buffer: Res, tonemapping_luts: Res, light_probes_buffer: Res, @@ -484,6 +485,7 @@ pub fn prepare_mesh_view_bind_groups( Some(light_probes_binding), Some(visibility_ranges_buffer), Some(ssr_binding), + Some(environment_map_binding), ) = ( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), @@ -493,11 +495,13 @@ pub fn prepare_mesh_view_bind_groups( light_probes_buffer.binding(), visibility_ranges.buffer().buffer(), ssr_buffer.binding(), + environment_map_uniform.binding(), ) { for ( entity, shadow_bindings, cluster_bindings, + msaa, ssao_textures, prepass_textures, transmission_texture, @@ -559,6 +563,7 @@ pub fn prepare_mesh_view_bind_groups( (15, diffuse_texture_view), (16, specular_texture_view), (17, sampler), + (18, environment_map_binding.clone()), )); } RenderViewEnvironmentMapBindGroupEntries::Multiple { @@ -570,6 +575,7 @@ pub fn prepare_mesh_view_bind_groups( (15, diffuse_texture_views.as_slice()), (16, specular_texture_views.as_slice()), (17, sampler), + (18, environment_map_binding.clone()), )); } } @@ -590,21 +596,21 @@ pub fn prepare_mesh_view_bind_groups( texture_view, sampler, }) => { - entries = entries.extend_with_indices(((18, texture_view), (19, sampler))); + entries = entries.extend_with_indices(((19, texture_view), (20, sampler))); } Some(RenderViewIrradianceVolumeBindGroupEntries::Multiple { ref texture_views, sampler, }) => { entries = entries - .extend_with_indices(((18, texture_views.as_slice()), (19, sampler))); + .extend_with_indices(((19, texture_views.as_slice()), (20, sampler))); } None => {} } let lut_bindings = get_lut_bindings(&images, &tonemapping_luts, tonemapping, &fallback_image); - entries = entries.extend_with_indices(((20, lut_bindings.0), (21, lut_bindings.1))); + entries = entries.extend_with_indices(((21, lut_bindings.0), (22, lut_bindings.1))); // When using WebGL, we can't have a depth texture with multisampling let prepass_bindings; @@ -614,7 +620,7 @@ pub fn prepare_mesh_view_bind_groups( for (binding, index) in prepass_bindings .iter() .map(Option::as_ref) - .zip([22, 23, 24, 25]) + .zip([23, 24, 25, 26]) .flat_map(|(b, i)| b.map(|b| (b, i))) { entries = entries.extend_with_indices(((index, binding),)); @@ -630,7 +636,7 @@ pub fn prepare_mesh_view_bind_groups( .unwrap_or(&fallback_image_zero.sampler); entries = - entries.extend_with_indices(((26, transmission_view), (27, transmission_sampler))); + entries.extend_with_indices(((27, transmission_view), (28, 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 f5fcd342715821..5036e81673e989 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -53,47 +53,48 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; @group(0) @binding(16) var specular_environment_map: texture_cube; #endif @group(0) @binding(17) var environment_map_sampler: sampler; +@group(0) @binding(18) var environment_map_uniform: types::EnvironmentMapUniform; #ifdef IRRADIANCE_VOLUMES_ARE_USABLE #ifdef MULTIPLE_LIGHT_PROBES_IN_ARRAY -@group(0) @binding(18) var irradiance_volumes: binding_array, 8u>; +@group(0) @binding(19) var irradiance_volumes: binding_array, 8u>; #else -@group(0) @binding(18) var irradiance_volume: texture_3d; +@group(0) @binding(19) var irradiance_volume: texture_3d; #endif -@group(0) @binding(19) var irradiance_volume_sampler: sampler; +@group(0) @binding(20) var irradiance_volume_sampler: sampler; #endif -@group(0) @binding(20) var dt_lut_texture: texture_3d; -@group(0) @binding(21) var dt_lut_sampler: sampler; +@group(0) @binding(21) var dt_lut_texture: texture_3d; +@group(0) @binding(22) var dt_lut_sampler: sampler; #ifdef MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(22) var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(23) var depth_prepass_texture: texture_depth_multisampled_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(23) var normal_prepass_texture: texture_multisampled_2d; +@group(0) @binding(24) var normal_prepass_texture: texture_multisampled_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(24) var motion_vector_prepass_texture: texture_multisampled_2d; +@group(0) @binding(25) var motion_vector_prepass_texture: texture_multisampled_2d; #endif // MOTION_VECTOR_PREPASS #else // MULTISAMPLED #ifdef DEPTH_PREPASS -@group(0) @binding(22) var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(23) var depth_prepass_texture: texture_depth_2d; #endif // DEPTH_PREPASS #ifdef NORMAL_PREPASS -@group(0) @binding(23) var normal_prepass_texture: texture_2d; +@group(0) @binding(24) var normal_prepass_texture: texture_2d; #endif // NORMAL_PREPASS #ifdef MOTION_VECTOR_PREPASS -@group(0) @binding(24) var motion_vector_prepass_texture: texture_2d; +@group(0) @binding(25) var motion_vector_prepass_texture: texture_2d; #endif // MOTION_VECTOR_PREPASS #endif // MULTISAMPLED #ifdef DEFERRED_PREPASS -@group(0) @binding(25) var deferred_prepass_texture: texture_2d; +@group(0) @binding(26) var deferred_prepass_texture: texture_2d; #endif // DEFERRED_PREPASS -@group(0) @binding(26) var view_transmission_texture: texture_2d; -@group(0) @binding(27) var view_transmission_sampler: sampler; +@group(0) @binding(27) var view_transmission_texture: texture_2d; +@group(0) @binding(28) 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 def738b3e28d5c..c1d379e3b4c6e3 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -148,3 +148,8 @@ struct ScreenSpaceReflectionsSettings { bisection_steps: u32, use_secant: u32, }; + +struct EnvironmentMapUniform { + // Transformation matrix for the environment cubemaps in world space. + transform: mat4x4, +}; \ No newline at end of file diff --git a/crates/bevy_pbr/src/render/morph.rs b/crates/bevy_pbr/src/render/morph.rs index 1aa82ec80e9f6f..d3d43e9b6988e5 100644 --- a/crates/bevy_pbr/src/render/morph.rs +++ b/crates/bevy_pbr/src/render/morph.rs @@ -1,4 +1,4 @@ -use std::{iter, mem}; +use std::{iter, mem, mem::size_of}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; @@ -83,7 +83,7 @@ const WGPU_MIN_ALIGN: usize = 256; /// Align a [`RawBufferVec`] to `N` bytes by padding the end with `T::default()` values. fn add_to_alignment(buffer: &mut RawBufferVec) { let n = WGPU_MIN_ALIGN; - let t_size = mem::size_of::(); + let t_size = size_of::(); if !can_align(n, t_size) { // This panic is stripped at compile time, due to n, t_size and can_align being const panic!( @@ -131,7 +131,7 @@ pub fn extract_morphs( uniform.current_buffer.extend(legal_weights); add_to_alignment::(&mut uniform.current_buffer); - let index = (start * mem::size_of::()) as u32; + let index = (start * size_of::()) as u32; morph_indices.current.insert(entity, MorphIndex { index }); } } diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index cd4db0fb28afa2..110d8c7fff8282 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -194,11 +194,12 @@ fn cascade_debug_visualization( ) -> vec3 { let overlay_alpha = 0.95; let cascade_index = get_cascade_index(light_id, view_z); - let cascade_color = hsv_to_rgb( + let cascade_color_hsv = vec3( f32(cascade_index) / f32(#{MAX_CASCADES_PER_LIGHT}u + 1u) * PI_2, 1.0, 0.5 ); + let cascade_color = hsv_to_rgb(cascade_color_hsv); return vec3( (1.0 - overlay_alpha) * output_color.rgb + overlay_alpha * cascade_color ); diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs index 86f702ea491d37..6d6f974b761ccb 100644 --- a/crates/bevy_pbr/src/render/skin.rs +++ b/crates/bevy_pbr/src/render/skin.rs @@ -1,4 +1,4 @@ -use std::mem; +use std::mem::{self, size_of}; use bevy_asset::Assets; use bevy_ecs::entity::EntityHashMap; @@ -26,7 +26,7 @@ impl SkinIndex { /// Index to be in address space based on the size of a skin uniform. const fn new(start: usize) -> Self { SkinIndex { - index: (start * std::mem::size_of::()) as u32, + index: (start * size_of::()) as u32, } } } diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 9c43854b94810b..cba48b8f8b1bd8 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -484,17 +484,16 @@ fn extract_ssao_settings( mut commands: Commands, cameras: Extract< Query< - (Entity, &Camera, &ScreenSpaceAmbientOcclusionSettings), + (Entity, &Camera, &ScreenSpaceAmbientOcclusionSettings, &Msaa), (With, With, With), >, >, - msaa: Extract>, ) { - for (entity, camera, ssao_settings) in &cameras { - if **msaa != Msaa::Off { + for (entity, camera, ssao_settings, msaa) in &cameras { + if *msaa != Msaa::Off { error!( "SSAO is being used which requires Msaa::Off, but Msaa is currently set to Msaa::{:?}", - **msaa + *msaa ); return; } diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 399c7820e5c2e3..1ae86b10a1aca1 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -43,7 +43,8 @@ use bevy_utils::{info_once, prelude::default}; use crate::{ binding_arrays_are_usable, graph::NodePbr, prelude::EnvironmentMapLight, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, RenderViewLightProbes, - ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, + ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, + ViewLightsUniformOffset, }; const SSR_SHADER_HANDLE: Handle = Handle::weak_from_u128(10438925299917978850); @@ -258,6 +259,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { Read, Read, Read, + Read, Read, Read, ); @@ -273,6 +275,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { view_fog_offset, view_light_probes_offset, view_ssr_offset, + view_environment_map_offset, view_bind_group, ssr_pipeline_id, ): QueryItem<'w, Self::ViewQuery>, @@ -324,6 +327,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { view_fog_offset.offset, **view_light_probes_offset, **view_ssr_offset, + **view_environment_map_offset, ], ); diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index d4dbbd3afeee1a..82aedd3d0b2770 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -30,56 +30,38 @@ //! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction use bevy_app::{App, Plugin}; -use bevy_asset::{load_internal_asset, Handle}; -use bevy_color::{Color, ColorToComponents}; -use bevy_core_pipeline::{ - core_3d::{ - graph::{Core3d, Node3d}, - prepare_core_3d_depth_textures, Camera3d, - }, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, - prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, +use bevy_asset::{load_internal_asset, Assets, Handle}; +use bevy_color::Color; +use bevy_core_pipeline::core_3d::{ + graph::{Core3d, Node3d}, + prepare_core_3d_depth_textures, }; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, - entity::Entity, - query::{Has, QueryItem, With}, - reflect::ReflectComponent, + bundle::Bundle, component::Component, reflect::ReflectComponent, schedule::IntoSystemConfigs as _, - system::{lifetimeless::Read, Commands, Query, Res, ResMut, Resource}, - world::{FromWorld, World}, }; -use bevy_math::Vec3; +use bevy_math::{ + primitives::{Cuboid, Plane3d}, + Vec2, Vec3, +}; use bevy_reflect::Reflect; use bevy_render::{ - render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner}, - render_resource::{ - binding_types::{ - sampler, texture_2d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, - }, - BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, - ColorTargetState, ColorWrites, DynamicUniformBuffer, FilterMode, FragmentState, - MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, - RenderPassDescriptor, RenderPipelineDescriptor, Sampler, SamplerBindingType, - SamplerDescriptor, Shader, ShaderStages, ShaderType, SpecializedRenderPipeline, - SpecializedRenderPipelines, TextureFormat, TextureSampleType, TextureUsages, - }, - renderer::{RenderContext, RenderDevice, RenderQueue}, - texture::BevyDefault, - view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, - Extract, ExtractSchedule, Render, RenderApp, RenderSet, + mesh::{Mesh, Meshable}, + render_graph::{RenderGraphApp, ViewNodeRunner}, + render_resource::{Shader, SpecializedRenderPipelines}, + texture::Image, + view::{InheritedVisibility, ViewVisibility, Visibility}, + ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::prelude::default; - -use crate::{ - graph::NodePbr, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, - ViewFogUniformOffset, ViewLightProbesUniformOffset, ViewLightsUniformOffset, - ViewScreenSpaceReflectionsUniformOffset, +use bevy_transform::components::{GlobalTransform, Transform}; +use render::{ + VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer, CUBE_MESH, PLANE_MESH, + VOLUMETRIC_FOG_HANDLE, }; -/// The volumetric fog shader. -pub const VOLUMETRIC_FOG_HANDLE: Handle = Handle::weak_from_u128(17400058287583986650); +use crate::graph::NodePbr; + +pub mod render; /// A plugin that implements volumetric fog. pub struct VolumetricFogPlugin; @@ -92,19 +74,12 @@ pub struct VolumetricFogPlugin; #[reflect(Component)] pub struct VolumetricLight; -/// When placed on a [`Camera3d`], enables volumetric fog and volumetric -/// lighting, also known as light shafts or god rays. +/// When placed on a [`bevy_core_pipeline::core_3d::Camera3d`], enables +/// volumetric fog and volumetric lighting, also known as light shafts or god +/// rays. #[derive(Clone, Copy, Component, Debug, Reflect)] #[reflect(Component)] pub struct VolumetricFogSettings { - /// The color of the fog. - /// - /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light - /// in order for this color to appear. - /// - /// Defaults to white. - pub fog_color: Color, - /// Color of the ambient light. /// /// This is separate from Bevy's [`AmbientLight`](crate::light::AmbientLight) because an @@ -124,6 +99,13 @@ pub struct VolumetricFogSettings { /// Defaults to 0.1. pub ambient_intensity: f32, + /// The maximum distance to offset the ray origin randomly by, in meters. + /// + /// This is intended for use with temporal antialiasing. It helps fog look + /// less blocky by varying the start position of the ray, using interleaved + /// gradient noise. + pub jitter: f32, + /// The number of raymarching steps to perform. /// /// Higher values produce higher-quality results with less banding, but @@ -131,16 +113,56 @@ pub struct VolumetricFogSettings { /// /// The default value is 64. pub step_count: u32, +} + +/// A convenient [`Bundle`] that contains all components necessary to generate a +/// fog volume. +#[derive(Bundle, Clone, Debug, Default)] +pub struct FogVolumeBundle { + /// The actual fog volume. + pub fog_volume: FogVolume, + /// Visibility. + pub visibility: Visibility, + /// Inherited visibility. + pub inherited_visibility: InheritedVisibility, + /// View visibility. + pub view_visibility: ViewVisibility, + /// The local transform. Set this to change the position, and scale of the + /// fog's axis-aligned bounding box (AABB). + pub transform: Transform, + /// The global transform. + pub global_transform: GlobalTransform, +} + +#[derive(Clone, Component, Debug, Reflect)] +#[reflect(Component)] +pub struct FogVolume { + /// The color of the fog. + /// + /// Note that the fog must be lit by a [`VolumetricLight`] or ambient light + /// in order for this color to appear. + /// + /// Defaults to white. + pub fog_color: Color, + + /// The density of fog, which measures how dark the fog is. + /// + /// The default value is 0.1. + pub density_factor: f32, + + /// Optional 3D voxel density texture for the fog. + pub density_texture: Option>, - /// The maximum distance that Bevy will trace a ray for, in world space. + /// Configurable offset of the density texture in UVW coordinates. /// - /// You can think of this as the radius of a sphere of fog surrounding the - /// camera. It has to be capped to a finite value or else there would be an - /// infinite amount of fog, which would result in completely-opaque areas - /// where the skybox would be. + /// This can be used to scroll a repeating density texture in a direction over time + /// to create effects like fog moving in the wind. Make sure to configure the texture + /// to use `ImageAddressMode::Repeat` if this is your intention. /// - /// The default value is 25. - pub max_depth: f32, + /// Has no effect when no density texture is present. + /// + /// The default value is (0, 0, 0). + pub density_texture_offset: Vec3, /// The absorption coefficient, which measures what fraction of light is /// absorbed by the fog at each step. @@ -156,12 +178,8 @@ pub struct VolumetricFogSettings { /// The default value is 0.3. pub scattering: f32, - /// The density of fog, which measures how dark the fog is. - /// - /// The default value is 0.1. - pub density: f32, - - /// Measures the fraction of light that's scattered *toward* the camera, as opposed to *away* from the camera. + /// Measures the fraction of light that's scattered *toward* the camera, as + /// opposed to *away* from the camera. /// /// Increasing this value makes light shafts become more prominent when the /// camera is facing toward their source and less prominent when the camera @@ -187,61 +205,6 @@ pub struct VolumetricFogSettings { pub light_intensity: f32, } -/// The GPU pipeline for the volumetric fog postprocessing effect. -#[derive(Resource)] -pub struct VolumetricFogPipeline { - /// A reference to the shared set of mesh pipeline view layouts. - mesh_view_layouts: MeshPipelineViewLayouts, - /// The view bind group when multisample antialiasing isn't in use. - volumetric_view_bind_group_layout_no_msaa: BindGroupLayout, - /// The view bind group when multisample antialiasing is in use. - volumetric_view_bind_group_layout_msaa: BindGroupLayout, - /// The sampler that we use to sample the postprocessing input. - color_sampler: Sampler, -} - -#[derive(Component, Deref, DerefMut)] -pub struct ViewVolumetricFogPipeline(pub CachedRenderPipelineId); - -/// The node in the render graph, part of the postprocessing stack, that -/// implements volumetric fog. -#[derive(Default)] -pub struct VolumetricFogNode; - -/// Identifies a single specialization of the volumetric fog shader. -#[derive(PartialEq, Eq, Hash, Clone, Copy)] -pub struct VolumetricFogPipelineKey { - /// The layout of the view, which is needed for the raymarching. - mesh_pipeline_view_key: MeshPipelineViewLayoutKey, - /// Whether the view has high dynamic range. - hdr: bool, -} - -/// The same as [`VolumetricFogSettings`], but formatted for the GPU. -#[derive(ShaderType)] -pub struct VolumetricFogUniform { - fog_color: Vec3, - light_tint: Vec3, - ambient_color: Vec3, - ambient_intensity: f32, - step_count: u32, - max_depth: f32, - absorption: f32, - scattering: f32, - density: f32, - scattering_asymmetry: f32, - light_intensity: f32, -} - -/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the -/// [`VolumetricFogUniform`] for a specific view. -#[derive(Component, Deref, DerefMut)] -pub struct ViewVolumetricFogUniformOffset(u32); - -/// The GPU buffer that stores the [`VolumetricFogUniform`] data. -#[derive(Resource, Default, Deref, DerefMut)] -pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); - impl Plugin for VolumetricFogPlugin { fn build(&self, app: &mut App) { load_internal_asset!( @@ -250,6 +213,11 @@ impl Plugin for VolumetricFogPlugin { "volumetric_fog.wgsl", Shader::from_wgsl ); + + let mut meshes = app.world_mut().resource_mut::>(); + meshes.insert(&PLANE_MESH, Plane3d::new(Vec3::Z, Vec2::ONE).mesh().into()); + meshes.insert(&CUBE_MESH, Cuboid::new(1.0, 1.0, 1.0).mesh().into()); + app.register_type::() .register_type::(); @@ -260,13 +228,13 @@ impl Plugin for VolumetricFogPlugin { render_app .init_resource::>() .init_resource::() - .add_systems(ExtractSchedule, extract_volumetric_fog) + .add_systems(ExtractSchedule, render::extract_volumetric_fog) .add_systems( Render, ( - prepare_volumetric_fog_pipelines.in_set(RenderSet::Prepare), - prepare_volumetric_fog_uniforms.in_set(RenderSet::Prepare), - prepare_view_depth_textures_for_volumetric_fog + render::prepare_volumetric_fog_pipelines.in_set(RenderSet::Prepare), + render::prepare_volumetric_fog_uniforms.in_set(RenderSet::Prepare), + render::prepare_view_depth_textures_for_volumetric_fog .in_set(RenderSet::Prepare) .before(prepare_core_3d_depth_textures), ), @@ -297,353 +265,26 @@ impl Default for VolumetricFogSettings { fn default() -> Self { Self { step_count: 64, - max_depth: 25.0, - absorption: 0.3, - scattering: 0.3, - density: 0.1, - scattering_asymmetry: 0.5, - fog_color: Color::WHITE, // Matches `AmbientLight` defaults. ambient_color: Color::WHITE, ambient_intensity: 0.1, - light_tint: Color::WHITE, - light_intensity: 1.0, - } - } -} - -impl FromWorld for VolumetricFogPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let mesh_view_layouts = world.resource::(); - - // Create the bind group layout entries common to both the MSAA and - // non-MSAA bind group layouts. - let base_bind_group_layout_entries = &*BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - // `volumetric_fog` - uniform_buffer::(true), - // `color_texture` - texture_2d(TextureSampleType::Float { filterable: true }), - // `color_sampler` - sampler(SamplerBindingType::Filtering), - ), - ); - - // Because `texture_depth_2d` and `texture_depth_2d_multisampled` are - // different types, we need to make separate bind group layouts for - // each. - - let mut bind_group_layout_entries_no_msaa = base_bind_group_layout_entries.to_vec(); - bind_group_layout_entries_no_msaa.extend_from_slice(&BindGroupLayoutEntries::with_indices( - ShaderStages::FRAGMENT, - ((3, texture_depth_2d()),), - )); - let volumetric_view_bind_group_layout_no_msaa = render_device.create_bind_group_layout( - "volumetric lighting view bind group layout", - &bind_group_layout_entries_no_msaa, - ); - - let mut bind_group_layout_entries_msaa = base_bind_group_layout_entries.to_vec(); - bind_group_layout_entries_msaa.extend_from_slice(&BindGroupLayoutEntries::with_indices( - ShaderStages::FRAGMENT, - ((3, texture_depth_2d_multisampled()),), - )); - let volumetric_view_bind_group_layout_msaa = render_device.create_bind_group_layout( - "volumetric lighting view bind group layout (multisampled)", - &bind_group_layout_entries_msaa, - ); - - let color_sampler = render_device.create_sampler(&SamplerDescriptor { - label: Some("volumetric lighting color sampler"), - mag_filter: FilterMode::Linear, - min_filter: FilterMode::Linear, - compare: None, - ..default() - }); - - VolumetricFogPipeline { - mesh_view_layouts: mesh_view_layouts.clone(), - volumetric_view_bind_group_layout_no_msaa, - volumetric_view_bind_group_layout_msaa, - color_sampler, + jitter: 0.0, } } } -/// Extracts [`VolumetricFogSettings`] and [`VolumetricLight`]s from the main -/// world to the render world. -pub fn extract_volumetric_fog( - mut commands: Commands, - view_targets: Extract>, - volumetric_lights: Extract>, -) { - if volumetric_lights.is_empty() { - return; - } - - for (view_target, volumetric_fog_settings) in view_targets.iter() { - commands - .get_or_spawn(view_target) - .insert(*volumetric_fog_settings); - } - - for (entity, volumetric_light) in volumetric_lights.iter() { - commands.get_or_spawn(entity).insert(*volumetric_light); - } -} - -impl ViewNode for VolumetricFogNode { - type ViewQuery = ( - Read, - Read, - Read, - Read, - Read, - Read, - Read, - Read, - Read, - Read, - ); - - fn run<'w>( - &self, - _: &mut RenderGraphContext, - render_context: &mut RenderContext<'w>, - ( - view_target, - view_depth_texture, - view_volumetric_lighting_pipeline, - view_uniform_offset, - view_lights_offset, - view_fog_offset, - view_light_probes_offset, - view_volumetric_lighting_uniform_buffer_offset, - view_bind_group, - view_ssr_offset, - ): QueryItem<'w, Self::ViewQuery>, - world: &'w World, - ) -> Result<(), NodeRunError> { - let pipeline_cache = world.resource::(); - let volumetric_lighting_pipeline = world.resource::(); - let volumetric_lighting_uniform_buffer = world.resource::(); - let msaa = world.resource::(); - - // Fetch the uniform buffer and binding. - let (Some(pipeline), Some(volumetric_lighting_uniform_buffer_binding)) = ( - pipeline_cache.get_render_pipeline(**view_volumetric_lighting_pipeline), - volumetric_lighting_uniform_buffer.binding(), - ) else { - return Ok(()); - }; - - let postprocess = view_target.post_process_write(); - - // Create the bind group for the view. - // - // TODO: Cache this. - let volumetric_view_bind_group_layout = match *msaa { - Msaa::Off => &volumetric_lighting_pipeline.volumetric_view_bind_group_layout_no_msaa, - _ => &volumetric_lighting_pipeline.volumetric_view_bind_group_layout_msaa, - }; - let volumetric_view_bind_group = render_context.render_device().create_bind_group( - None, - volumetric_view_bind_group_layout, - &BindGroupEntries::sequential(( - volumetric_lighting_uniform_buffer_binding, - postprocess.source, - &volumetric_lighting_pipeline.color_sampler, - view_depth_texture.view(), - )), - ); - - let render_pass_descriptor = RenderPassDescriptor { - label: Some("volumetric lighting pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view: postprocess.destination, - resolve_target: None, - ops: Operations::default(), - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }; - - let mut render_pass = render_context - .command_encoder() - .begin_render_pass(&render_pass_descriptor); - - render_pass.set_pipeline(pipeline); - render_pass.set_bind_group( - 0, - &view_bind_group.value, - &[ - view_uniform_offset.offset, - view_lights_offset.offset, - view_fog_offset.offset, - **view_light_probes_offset, - **view_ssr_offset, - ], - ); - render_pass.set_bind_group( - 1, - &volumetric_view_bind_group, - &[**view_volumetric_lighting_uniform_buffer_offset], - ); - render_pass.draw(0..3, 0..1); - - Ok(()) - } -} - -impl SpecializedRenderPipeline for VolumetricFogPipeline { - type Key = VolumetricFogPipelineKey; - - fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - let mesh_view_layout = self - .mesh_view_layouts - .get_view_layout(key.mesh_pipeline_view_key); - - // We always use hardware 2x2 filtering for sampling the shadow map; the - // more accurate versions with percentage-closer filtering aren't worth - // the overhead. - let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()]; - - // We need a separate layout for MSAA and non-MSAA. - let volumetric_view_bind_group_layout = if key - .mesh_pipeline_view_key - .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) - { - shader_defs.push("MULTISAMPLED".into()); - self.volumetric_view_bind_group_layout_msaa.clone() - } else { - self.volumetric_view_bind_group_layout_no_msaa.clone() - }; - - RenderPipelineDescriptor { - label: Some("volumetric lighting pipeline".into()), - layout: vec![mesh_view_layout.clone(), volumetric_view_bind_group_layout], - push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), - primitive: PrimitiveState::default(), - depth_stencil: None, - multisample: MultisampleState::default(), - fragment: Some(FragmentState { - shader: VOLUMETRIC_FOG_HANDLE, - shader_defs, - entry_point: "fragment".into(), - targets: vec![Some(ColorTargetState { - format: if key.hdr { - ViewTarget::TEXTURE_FORMAT_HDR - } else { - TextureFormat::bevy_default() - }, - blend: None, - write_mask: ColorWrites::ALL, - })], - }), +impl Default for FogVolume { + fn default() -> Self { + Self { + absorption: 0.3, + scattering: 0.3, + density_factor: 0.1, + density_texture: None, + density_texture_offset: Vec3::ZERO, + scattering_asymmetry: 0.5, + fog_color: Color::WHITE, + light_tint: Color::WHITE, + light_intensity: 1.0, } } } - -/// Specializes volumetric fog pipelines for all views with that effect enabled. -pub fn prepare_volumetric_fog_pipelines( - mut commands: Commands, - pipeline_cache: Res, - mut pipelines: ResMut>, - volumetric_lighting_pipeline: Res, - view_targets: Query< - ( - Entity, - &ExtractedView, - Has, - Has, - Has, - Has, - ), - With, - >, - msaa: Res, -) { - for (entity, view, normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass) in - view_targets.iter() - { - // Create a mesh pipeline view layout key corresponding to the view. - let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa); - mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass); - mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass); - mesh_pipeline_view_key.set( - MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, - motion_vector_prepass, - ); - mesh_pipeline_view_key.set( - MeshPipelineViewLayoutKey::DEFERRED_PREPASS, - deferred_prepass, - ); - - // Specialize the pipeline. - let pipeline_id = pipelines.specialize( - &pipeline_cache, - &volumetric_lighting_pipeline, - VolumetricFogPipelineKey { - mesh_pipeline_view_key, - hdr: view.hdr, - }, - ); - - commands - .entity(entity) - .insert(ViewVolumetricFogPipeline(pipeline_id)); - } -} - -/// A system that converts [`VolumetricFogSettings`] -pub fn prepare_volumetric_fog_uniforms( - mut commands: Commands, - mut volumetric_lighting_uniform_buffer: ResMut, - view_targets: Query<(Entity, &VolumetricFogSettings)>, - render_device: Res, - render_queue: Res, -) { - let Some(mut writer) = volumetric_lighting_uniform_buffer.get_writer( - view_targets.iter().len(), - &render_device, - &render_queue, - ) else { - return; - }; - - for (entity, volumetric_fog_settings) in view_targets.iter() { - let offset = writer.write(&VolumetricFogUniform { - fog_color: volumetric_fog_settings.fog_color.to_linear().to_vec3(), - light_tint: volumetric_fog_settings.light_tint.to_linear().to_vec3(), - ambient_color: volumetric_fog_settings.ambient_color.to_linear().to_vec3(), - ambient_intensity: volumetric_fog_settings.ambient_intensity, - step_count: volumetric_fog_settings.step_count, - max_depth: volumetric_fog_settings.max_depth, - absorption: volumetric_fog_settings.absorption, - scattering: volumetric_fog_settings.scattering, - density: volumetric_fog_settings.density, - scattering_asymmetry: volumetric_fog_settings.scattering_asymmetry, - light_intensity: volumetric_fog_settings.light_intensity, - }); - - commands - .entity(entity) - .insert(ViewVolumetricFogUniformOffset(offset)); - } -} - -/// A system that marks all view depth textures as readable in shaders. -/// -/// The volumetric lighting pass needs to do this, and it doesn't happen by -/// default. -pub fn prepare_view_depth_textures_for_volumetric_fog( - mut view_targets: Query<&mut Camera3d, With>, -) { - for mut camera in view_targets.iter_mut() { - camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); - } -} diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs new file mode 100644 index 00000000000000..618e984db05f7e --- /dev/null +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -0,0 +1,854 @@ +//! Rendering of fog volumes. + +use std::array; + +use bevy_asset::{AssetId, Handle}; +use bevy_color::ColorToComponents as _; +use bevy_core_pipeline::{ + core_3d::Camera3d, + prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, +}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Has, QueryItem, With}, + system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::{vec4, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles as _}; +use bevy_render::{ + mesh::{ + allocator::MeshAllocator, Mesh, MeshVertexBufferLayoutRef, RenderMesh, RenderMeshBufferInfo, + }, + render_asset::RenderAssets, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{ + sampler, texture_3d, texture_depth_2d, texture_depth_2d_multisampled, uniform_buffer, + }, + BindGroupLayout, BindGroupLayoutEntries, BindingResource, BlendComponent, BlendFactor, + BlendOperation, BlendState, CachedRenderPipelineId, ColorTargetState, ColorWrites, + DynamicBindGroupEntries, DynamicUniformBuffer, Face, FragmentState, LoadOp, + MultisampleState, Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, + RenderPassDescriptor, RenderPipelineDescriptor, SamplerBindingType, Shader, ShaderStages, + ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, StoreOp, TextureFormat, + TextureSampleType, TextureUsages, VertexState, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::{BevyDefault as _, GpuImage, Image}, + view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + Extract, +}; +use bevy_transform::components::GlobalTransform; +use bevy_utils::prelude::default; +use bitflags::bitflags; + +use crate::{ + FogVolume, MeshPipelineViewLayoutKey, MeshPipelineViewLayouts, MeshViewBindGroup, + ViewEnvironmentMapUniformOffset, ViewFogUniformOffset, ViewLightProbesUniformOffset, + ViewLightsUniformOffset, ViewScreenSpaceReflectionsUniformOffset, VolumetricFogSettings, + VolumetricLight, +}; + +bitflags! { + /// Flags that describe the bind group layout used to render volumetric fog. + #[derive(Clone, Copy, PartialEq)] + struct VolumetricFogBindGroupLayoutKey: u8 { + /// The framebuffer is multisampled. + const MULTISAMPLED = 0x1; + /// The volumetric fog has a 3D voxel density texture. + const DENSITY_TEXTURE = 0x2; + } +} + +bitflags! { + /// Flags that describe the rasterization pipeline used to render volumetric + /// fog. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + struct VolumetricFogPipelineKeyFlags: u8 { + /// The view's color format has high dynamic range. + const HDR = 0x1; + /// The volumetric fog has a 3D voxel density texture. + const DENSITY_TEXTURE = 0x2; + } +} + +/// The volumetric fog shader. +pub const VOLUMETRIC_FOG_HANDLE: Handle = Handle::weak_from_u128(17400058287583986650); + +/// The plane mesh, which is used to render a fog volume that the camera is +/// inside. +/// +/// This mesh is simply stretched to the size of the framebuffer, as when the +/// camera is inside a fog volume it's essentially a full-screen effect. +pub const PLANE_MESH: Handle = Handle::weak_from_u128(435245126479971076); + +/// The cube mesh, which is used to render a fog volume that the camera is +/// outside. +/// +/// Note that only the front faces of this cuboid will be rasterized in +/// hardware. The back faces will be calculated in the shader via raytracing. +pub const CUBE_MESH: Handle = Handle::weak_from_u128(5023959819001661507); + +/// The total number of bind group layouts. +/// +/// This is the total number of combinations of all +/// [`VolumetricFogBindGroupLayoutKey`] flags. +const VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT: usize = + VolumetricFogBindGroupLayoutKey::all().bits() as usize + 1; + +/// A matrix that converts from local 1×1×1 space to UVW 3D density texture +/// space. +static UVW_FROM_LOCAL: Mat4 = Mat4::from_cols( + vec4(1.0, 0.0, 0.0, 0.0), + vec4(0.0, 1.0, 0.0, 0.0), + vec4(0.0, 0.0, 1.0, 0.0), + vec4(0.5, 0.5, 0.5, 1.0), +); + +/// The GPU pipeline for the volumetric fog postprocessing effect. +#[derive(Resource)] +pub struct VolumetricFogPipeline { + /// A reference to the shared set of mesh pipeline view layouts. + mesh_view_layouts: MeshPipelineViewLayouts, + + /// All bind group layouts. + /// + /// Since there aren't too many of these, we precompile them all. + volumetric_view_bind_group_layouts: [BindGroupLayout; VOLUMETRIC_FOG_BIND_GROUP_LAYOUT_COUNT], +} + +/// The two render pipelines that we use for fog volumes: one for when a 3D +/// density texture is present and one for when it isn't. +#[derive(Component)] +pub struct ViewVolumetricFogPipelines { + /// The render pipeline that we use when no density texture is present, and + /// the density distribution is uniform. + pub textureless: CachedRenderPipelineId, + /// The render pipeline that we use when a density texture is present. + pub textured: CachedRenderPipelineId, +} + +/// The node in the render graph, part of the postprocessing stack, that +/// implements volumetric fog. +#[derive(Default)] +pub struct VolumetricFogNode; + +/// Identifies a single specialization of the volumetric fog shader. +#[derive(PartialEq, Eq, Hash, Clone)] +pub struct VolumetricFogPipelineKey { + /// The layout of the view, which is needed for the raymarching. + mesh_pipeline_view_key: MeshPipelineViewLayoutKey, + + /// The vertex buffer layout of the primitive. + /// + /// Both planes (used when the camera is inside the fog volume) and cubes + /// (used when the camera is outside the fog volume) use identical vertex + /// buffer layouts, so we only need one of them. + vertex_buffer_layout: MeshVertexBufferLayoutRef, + + /// Flags that specify features on the pipeline key. + flags: VolumetricFogPipelineKeyFlags, +} + +/// The same as [`VolumetricFogSettings`] and [`FogVolume`], but formatted for +/// the GPU. +/// +/// See the documentation of those structures for more information on these +/// fields. +#[derive(ShaderType)] +pub struct VolumetricFogUniform { + clip_from_local: Mat4, + + /// The transform from world space to 3D density texture UVW space. + uvw_from_world: Mat4, + + /// View-space plane equations of the far faces of the fog volume cuboid. + /// + /// The vector takes the form V = (N, -N⋅Q), where N is the normal of the + /// plane and Q is any point in it, in view space. The equation of the plane + /// for homogeneous point P = (Px, Py, Pz, Pw) is V⋅P = 0. + far_planes: [Vec4; 3], + + fog_color: Vec3, + light_tint: Vec3, + ambient_color: Vec3, + ambient_intensity: f32, + step_count: u32, + + /// The radius of a sphere that bounds the fog volume in view space. + bounding_radius: f32, + + absorption: f32, + scattering: f32, + density: f32, + density_texture_offset: Vec3, + scattering_asymmetry: f32, + light_intensity: f32, + jitter_strength: f32, +} + +/// Specifies the offset within the [`VolumetricFogUniformBuffer`] of the +/// [`VolumetricFogUniform`] for a specific view. +#[derive(Component, Deref, DerefMut)] +pub struct ViewVolumetricFog(Vec); + +/// Information that the render world needs to maintain about each fog volume. +pub struct ViewFogVolume { + /// The 3D voxel density texture for this volume, if present. + density_texture: Option>, + /// The offset of this view's [`VolumetricFogUniform`] structure within the + /// [`VolumetricFogUniformBuffer`]. + uniform_buffer_offset: u32, + /// True if the camera is outside the fog volume; false if it's inside the + /// fog volume. + exterior: bool, +} + +/// The GPU buffer that stores the [`VolumetricFogUniform`] data. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); + +impl FromWorld for VolumetricFogPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let mesh_view_layouts = world.resource::(); + + // Create the bind group layout entries common to all bind group + // layouts. + let base_bind_group_layout_entries = &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + // `volumetric_fog` + uniform_buffer::(true), + ); + + // For every combination of `VolumetricFogBindGroupLayoutKey` bits, + // create a bind group layout. + let bind_group_layouts = array::from_fn(|bits| { + let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8); + + let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec(); + + // `depth_texture` + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + (( + 1, + if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) { + texture_depth_2d_multisampled() + } else { + texture_depth_2d() + }, + ),), + )); + + // `density_texture` and `density_sampler` + if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) { + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + ( + (2, texture_3d(TextureSampleType::Float { filterable: true })), + (3, sampler(SamplerBindingType::Filtering)), + ), + )); + } + + // Create the bind group layout. + let description = flags.bind_group_layout_description(); + render_device.create_bind_group_layout(&*description, &bind_group_layout_entries) + }); + + VolumetricFogPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + volumetric_view_bind_group_layouts: bind_group_layouts, + } + } +} + +/// Extracts [`VolumetricFogSettings`], [`FogVolume`], and [`VolumetricLight`]s +/// from the main world to the render world. +pub fn extract_volumetric_fog( + mut commands: Commands, + view_targets: Extract>, + fog_volumes: Extract>, + volumetric_lights: Extract>, +) { + if volumetric_lights.is_empty() { + return; + } + + for (entity, volumetric_fog_settings) in view_targets.iter() { + commands + .get_or_spawn(entity) + .insert(*volumetric_fog_settings); + } + + for (entity, fog_volume, fog_transform) in fog_volumes.iter() { + commands + .get_or_spawn(entity) + .insert((*fog_volume).clone()) + .insert(*fog_transform); + } + + for (entity, volumetric_light) in volumetric_lights.iter() { + commands.get_or_spawn(entity).insert(*volumetric_light); + } +} + +impl ViewNode for VolumetricFogNode { + type ViewQuery = ( + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + Read, + ); + + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + ( + view_target, + view_depth_texture, + view_volumetric_lighting_pipelines, + view_uniform_offset, + view_lights_offset, + view_fog_offset, + view_light_probes_offset, + view_fog_volumes, + view_bind_group, + view_ssr_offset, + msaa, + view_environment_map_offset, + ): QueryItem<'w, Self::ViewQuery>, + world: &'w World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let volumetric_lighting_pipeline = world.resource::(); + let volumetric_lighting_uniform_buffers = world.resource::(); + let image_assets = world.resource::>(); + let mesh_allocator = world.resource::(); + + // Fetch the uniform buffer and binding. + let ( + Some(textureless_pipeline), + Some(textured_pipeline), + Some(volumetric_lighting_uniform_buffer_binding), + ) = ( + pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textureless), + pipeline_cache.get_render_pipeline(view_volumetric_lighting_pipelines.textured), + volumetric_lighting_uniform_buffers.binding(), + ) + else { + return Ok(()); + }; + + let render_meshes = world.resource::>(); + + for view_fog_volume in view_fog_volumes.iter() { + // If the camera is outside the fog volume, pick the cube mesh; + // otherwise, pick the plane mesh. In the latter case we'll be + // effectively rendering a full-screen quad. + let mesh_handle = if view_fog_volume.exterior { + CUBE_MESH.clone() + } else { + PLANE_MESH.clone() + }; + + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(&mesh_handle.id()) + else { + continue; + }; + + let density_image = view_fog_volume + .density_texture + .and_then(|density_texture| image_assets.get(density_texture)); + + // Pick the right pipeline, depending on whether a density texture + // is present or not. + let pipeline = if density_image.is_some() { + textured_pipeline + } else { + textureless_pipeline + }; + + // This should always succeed, but if the asset was unloaded don't + // panic. + let Some(render_mesh) = render_meshes.get(&mesh_handle) else { + return Ok(()); + }; + + // Create the bind group for the view. + // + // TODO: Cache this. + + let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty(); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::MULTISAMPLED, + !matches!(*msaa, Msaa::Off), + ); + + // Create the bind group entries. The ones relating to the density + // texture will only be filled in if that texture is present. + let mut bind_group_entries = DynamicBindGroupEntries::sequential(( + volumetric_lighting_uniform_buffer_binding.clone(), + BindingResource::TextureView(view_depth_texture.view()), + )); + if let Some(density_image) = density_image { + bind_group_layout_key.insert(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE); + bind_group_entries = bind_group_entries.extend_sequential(( + BindingResource::TextureView(&density_image.texture_view), + BindingResource::Sampler(&density_image.sampler), + )); + } + + let volumetric_view_bind_group_layout = &volumetric_lighting_pipeline + .volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize]; + + let volumetric_view_bind_group = render_context.render_device().create_bind_group( + None, + volumetric_view_bind_group_layout, + &bind_group_entries, + ); + + let render_pass_descriptor = RenderPassDescriptor { + label: Some("volumetric lighting pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: view_target.main_texture_view(), + resolve_target: None, + ops: Operations { + load: LoadOp::Load, + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; + + let mut render_pass = render_context + .command_encoder() + .begin_render_pass(&render_pass_descriptor); + + render_pass.set_vertex_buffer(0, *vertex_buffer_slice.buffer.slice(..)); + render_pass.set_pipeline(pipeline); + render_pass.set_bind_group( + 0, + &view_bind_group.value, + &[ + view_uniform_offset.offset, + view_lights_offset.offset, + view_fog_offset.offset, + **view_light_probes_offset, + **view_ssr_offset, + **view_environment_map_offset, + ], + ); + render_pass.set_bind_group( + 1, + &volumetric_view_bind_group, + &[view_fog_volume.uniform_buffer_offset], + ); + + // Draw elements or arrays, as appropriate. + match &render_mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + index_format, + count, + } => { + let Some(index_buffer_slice) = + mesh_allocator.mesh_index_slice(&mesh_handle.id()) + else { + continue; + }; + + render_pass + .set_index_buffer(*index_buffer_slice.buffer.slice(..), *index_format); + render_pass.draw_indexed( + index_buffer_slice.range.start..(index_buffer_slice.range.start + count), + vertex_buffer_slice.range.start as i32, + 0..1, + ); + } + RenderMeshBufferInfo::NonIndexed => { + render_pass.draw(vertex_buffer_slice.range, 0..1); + } + } + } + + Ok(()) + } +} + +impl SpecializedRenderPipeline for VolumetricFogPipeline { + type Key = VolumetricFogPipelineKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let mesh_view_layout = self + .mesh_view_layouts + .get_view_layout(key.mesh_pipeline_view_key); + + // We always use hardware 2x2 filtering for sampling the shadow map; the + // more accurate versions with percentage-closer filtering aren't worth + // the overhead. + let mut shader_defs = vec!["SHADOW_FILTER_METHOD_HARDWARE_2X2".into()]; + + // We need a separate layout for MSAA and non-MSAA, as well as one for + // the presence or absence of the density texture. + let mut bind_group_layout_key = VolumetricFogBindGroupLayoutKey::empty(); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::MULTISAMPLED, + key.mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED), + ); + bind_group_layout_key.set( + VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE, + key.flags + .contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE), + ); + + let volumetric_view_bind_group_layout = + self.volumetric_view_bind_group_layouts[bind_group_layout_key.bits() as usize].clone(); + + // Both the cube and plane have the same vertex layout, so we don't need + // to distinguish between the two. + let vertex_format = key + .vertex_buffer_layout + .0 + .get_layout(&[Mesh::ATTRIBUTE_POSITION.at_shader_location(0)]) + .expect("Failed to get vertex layout for volumetric fog hull"); + + if key + .mesh_pipeline_view_key + .contains(MeshPipelineViewLayoutKey::MULTISAMPLED) + { + shader_defs.push("MULTISAMPLED".into()); + } + + if key + .flags + .contains(VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE) + { + shader_defs.push("DENSITY_TEXTURE".into()); + } + + RenderPipelineDescriptor { + label: Some("volumetric lighting pipeline".into()), + layout: vec![mesh_view_layout.clone(), volumetric_view_bind_group_layout], + push_constant_ranges: vec![], + vertex: VertexState { + shader: VOLUMETRIC_FOG_HANDLE, + shader_defs: shader_defs.clone(), + entry_point: "vertex".into(), + buffers: vec![vertex_format], + }, + primitive: PrimitiveState { + cull_mode: Some(Face::Back), + ..default() + }, + depth_stencil: None, + multisample: MultisampleState::default(), + fragment: Some(FragmentState { + shader: VOLUMETRIC_FOG_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.flags.contains(VolumetricFogPipelineKeyFlags::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + // Blend on top of what's already in the framebuffer. Doing + // the alpha blending with the hardware blender allows us to + // avoid having to use intermediate render targets. + blend: Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::One, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent { + src_factor: BlendFactor::Zero, + dst_factor: BlendFactor::One, + operation: BlendOperation::Add, + }, + }), + write_mask: ColorWrites::ALL, + })], + }), + } + } +} + +/// Specializes volumetric fog pipelines for all views with that effect enabled. +#[allow(clippy::too_many_arguments)] +pub fn prepare_volumetric_fog_pipelines( + mut commands: Commands, + pipeline_cache: Res, + mut pipelines: ResMut>, + volumetric_lighting_pipeline: Res, + view_targets: Query< + ( + Entity, + &ExtractedView, + &Msaa, + Has, + Has, + Has, + Has, + ), + With, + >, + meshes: Res>, +) { + let plane_mesh = meshes.get(&PLANE_MESH).expect("Plane mesh not found!"); + + for ( + entity, + view, + msaa, + normal_prepass, + depth_prepass, + motion_vector_prepass, + deferred_prepass, + ) in view_targets.iter() + { + // Create a mesh pipeline view layout key corresponding to the view. + let mut mesh_pipeline_view_key = MeshPipelineViewLayoutKey::from(*msaa); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::NORMAL_PREPASS, normal_prepass); + mesh_pipeline_view_key.set(MeshPipelineViewLayoutKey::DEPTH_PREPASS, depth_prepass); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::MOTION_VECTOR_PREPASS, + motion_vector_prepass, + ); + mesh_pipeline_view_key.set( + MeshPipelineViewLayoutKey::DEFERRED_PREPASS, + deferred_prepass, + ); + + let mut textureless_flags = VolumetricFogPipelineKeyFlags::empty(); + textureless_flags.set(VolumetricFogPipelineKeyFlags::HDR, view.hdr); + + // Specialize the pipeline. + let textureless_pipeline_key = VolumetricFogPipelineKey { + mesh_pipeline_view_key, + vertex_buffer_layout: plane_mesh.layout.clone(), + flags: textureless_flags, + }; + let textureless_pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + textureless_pipeline_key.clone(), + ); + let textured_pipeline_id = pipelines.specialize( + &pipeline_cache, + &volumetric_lighting_pipeline, + VolumetricFogPipelineKey { + flags: textureless_pipeline_key.flags + | VolumetricFogPipelineKeyFlags::DENSITY_TEXTURE, + ..textureless_pipeline_key + }, + ); + + commands.entity(entity).insert(ViewVolumetricFogPipelines { + textureless: textureless_pipeline_id, + textured: textured_pipeline_id, + }); + } +} + +/// A system that converts [`VolumetricFogSettings`] into [`VolumetricFogUniform`]s. +pub fn prepare_volumetric_fog_uniforms( + mut commands: Commands, + mut volumetric_lighting_uniform_buffer: ResMut, + view_targets: Query<(Entity, &ExtractedView, &VolumetricFogSettings)>, + fog_volumes: Query<(Entity, &FogVolume, &GlobalTransform)>, + render_device: Res, + render_queue: Res, + mut local_from_world_matrices: Local>, +) { + let Some(mut writer) = volumetric_lighting_uniform_buffer.get_writer( + view_targets.iter().len(), + &render_device, + &render_queue, + ) else { + return; + }; + + // Do this up front to avoid O(n^2) matrix inversion. + local_from_world_matrices.clear(); + for (_, _, fog_transform) in fog_volumes.iter() { + local_from_world_matrices.push(fog_transform.compute_matrix().inverse()); + } + + for (view_entity, extracted_view, volumetric_fog_settings) in view_targets.iter() { + let world_from_view = extracted_view.world_from_view.compute_matrix(); + + let mut view_fog_volumes = vec![]; + + for ((_, fog_volume, _), local_from_world) in + fog_volumes.iter().zip(local_from_world_matrices.iter()) + { + // Calculate the transforms to and from 1×1×1 local space. + let local_from_view = *local_from_world * world_from_view; + let view_from_local = local_from_view.inverse(); + + // Determine whether the camera is inside or outside the volume, and + // calculate the clip space transform. + let interior = camera_is_inside_fog_volume(&local_from_view); + let hull_clip_from_local = calculate_fog_volume_clip_from_local_transforms( + interior, + &extracted_view.clip_from_view, + &view_from_local, + ); + + // Calculate the radius of the sphere that bounds the fog volume. + let bounding_radius = (Mat3A::from_mat4(view_from_local) * Vec3A::splat(0.5)).length(); + + // Write out our uniform. + let uniform_buffer_offset = writer.write(&VolumetricFogUniform { + clip_from_local: hull_clip_from_local, + uvw_from_world: UVW_FROM_LOCAL * *local_from_world, + far_planes: get_far_planes(&view_from_local), + fog_color: fog_volume.fog_color.to_linear().to_vec3(), + light_tint: fog_volume.light_tint.to_linear().to_vec3(), + ambient_color: volumetric_fog_settings.ambient_color.to_linear().to_vec3(), + ambient_intensity: volumetric_fog_settings.ambient_intensity, + step_count: volumetric_fog_settings.step_count, + bounding_radius, + absorption: fog_volume.absorption, + scattering: fog_volume.scattering, + density: fog_volume.density_factor, + density_texture_offset: fog_volume.density_texture_offset, + scattering_asymmetry: fog_volume.scattering_asymmetry, + light_intensity: fog_volume.light_intensity, + jitter_strength: volumetric_fog_settings.jitter, + }); + + view_fog_volumes.push(ViewFogVolume { + uniform_buffer_offset, + exterior: !interior, + density_texture: fog_volume.density_texture.as_ref().map(Handle::id), + }); + } + + commands + .entity(view_entity) + .insert(ViewVolumetricFog(view_fog_volumes)); + } +} + +/// A system that marks all view depth textures as readable in shaders. +/// +/// The volumetric lighting pass needs to do this, and it doesn't happen by +/// default. +pub fn prepare_view_depth_textures_for_volumetric_fog( + mut view_targets: Query<&mut Camera3d>, + fog_volumes: Query<&VolumetricFogSettings>, +) { + if fog_volumes.is_empty() { + return; + } + + for mut camera in view_targets.iter_mut() { + camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); + } +} + +fn get_far_planes(view_from_local: &Mat4) -> [Vec4; 3] { + let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0); + let view_from_normal_local = Mat3A::from_mat4(*view_from_local); + + for &local_normal in &[ + Vec3A::X, + Vec3A::NEG_X, + Vec3A::Y, + Vec3A::NEG_Y, + Vec3A::Z, + Vec3A::NEG_Z, + ] { + let view_normal = (view_from_normal_local * local_normal).normalize_or_zero(); + if view_normal.z <= 0.0 { + continue; + } + + let view_position = *view_from_local * (-local_normal * 0.5).extend(1.0); + let plane_coords = view_normal.extend(-view_normal.dot(view_position.xyz().into())); + + far_planes[next_index] = plane_coords; + next_index += 1; + if next_index == far_planes.len() { + continue; + } + } + + far_planes +} + +impl VolumetricFogBindGroupLayoutKey { + /// Creates an appropriate debug description for the bind group layout with + /// these flags. + fn bind_group_layout_description(&self) -> String { + if self.is_empty() { + return "volumetric lighting view bind group layout".to_owned(); + } + + format!( + "volumetric lighting view bind group layout ({})", + self.iter() + .filter_map(|flag| { + if flag == VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE { + Some("density texture") + } else if flag == VolumetricFogBindGroupLayoutKey::MULTISAMPLED { + Some("multisampled") + } else { + None + } + }) + .collect::>() + .join(", ") + ) + } +} + +/// Given the transform from the view to the 1×1×1 cube in local fog volume +/// space, returns true if the camera is inside the volume. +fn camera_is_inside_fog_volume(local_from_view: &Mat4) -> bool { + Vec3A::from(local_from_view.col(3).xyz()) + .abs() + .cmple(Vec3A::splat(0.5)) + .all() +} + +/// Given the local transforms, returns the matrix that transforms model space +/// to clip space. +fn calculate_fog_volume_clip_from_local_transforms( + interior: bool, + clip_from_view: &Mat4, + view_from_local: &Mat4, +) -> Mat4 { + if !interior { + return *clip_from_view * *view_from_local; + } + + // If the camera is inside the fog volume, then we'll be rendering a full + // screen quad. The shader will start its raymarch at the fragment depth + // value, however, so we need to make sure that the depth of the full screen + // quad is at the near clip plane `z_near`. + let z_near = clip_from_view.w_axis[2]; + Mat4::from_cols( + vec4(z_near, 0.0, 0.0, 0.0), + vec4(0.0, z_near, 0.0, 0.0), + vec4(0.0, 0.0, 0.0, 0.0), + vec4(0.0, 0.0, z_near, z_near), + ) +} diff --git a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl index 0ea6c18f7c46b8..2c30eddbeaf8bd 100644 --- a/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl +++ b/crates/bevy_pbr/src/volumetric_fog/volumetric_fog.wgsl @@ -2,56 +2,79 @@ // sampling directional light shadow maps. // // The overall approach is a combination of the volumetric rendering in [1] and -// the shadow map raymarching in [2]. First, we sample the depth buffer to -// determine how long our ray is. Then we do a raymarch, with physically-based -// calculations at each step to determine how much light was absorbed, scattered -// out, and scattered in. To determine in-scattering, we sample the shadow map -// for the light to determine whether the point was in shadow or not. +// the shadow map raymarching in [2]. First, we raytrace the AABB of the fog +// volume in order to determine how long our ray is. Then we do a raymarch, with +// physically-based calculations at each step to determine how much light was +// absorbed, scattered out, and scattered in. To determine in-scattering, we +// sample the shadow map for the light to determine whether the point was in +// shadow or not. // // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html // // [2]: http://www.alexandre-pestana.com/volumetric-lights/ #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput -#import bevy_pbr::mesh_view_bindings::{lights, view} +#import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip} +#import bevy_pbr::mesh_view_bindings::{globals, lights, view} #import bevy_pbr::mesh_view_types::DIRECTIONAL_LIGHT_FLAGS_VOLUMETRIC_BIT #import bevy_pbr::shadow_sampling::sample_shadow_map_hardware #import bevy_pbr::shadows::{get_cascade_index, world_to_directional_light_local} +#import bevy_pbr::utils::interleaved_gradient_noise #import bevy_pbr::view_transformations::{ + depth_ndc_to_view_z, frag_coord_to_ndc, position_ndc_to_view, - position_ndc_to_world + position_ndc_to_world, + position_view_to_world } // The GPU version of [`VolumetricFogSettings`]. See the comments in // `volumetric_fog/mod.rs` for descriptions of the fields here. struct VolumetricFog { + clip_from_local: mat4x4, + uvw_from_world: mat4x4, + far_planes: array, 3>, fog_color: vec3, light_tint: vec3, ambient_color: vec3, ambient_intensity: f32, step_count: u32, - max_depth: f32, + bounding_radius: f32, absorption: f32, scattering: f32, - density: f32, + density_factor: f32, + density_texture_offset: vec3, scattering_asymmetry: f32, light_intensity: f32, + jitter_strength: f32, } @group(1) @binding(0) var volumetric_fog: VolumetricFog; -@group(1) @binding(1) var color_texture: texture_2d; -@group(1) @binding(2) var color_sampler: sampler; #ifdef MULTISAMPLED -@group(1) @binding(3) var depth_texture: texture_depth_multisampled_2d; +@group(1) @binding(1) var depth_texture: texture_depth_multisampled_2d; #else -@group(1) @binding(3) var depth_texture: texture_depth_2d; +@group(1) @binding(1) var depth_texture: texture_depth_2d; #endif +#ifdef DENSITY_TEXTURE +@group(1) @binding(2) var density_texture: texture_3d; +@group(1) @binding(3) var density_sampler: sampler; +#endif // DENSITY_TEXTURE + // 1 / (4π) const FRAC_4_PI: f32 = 0.07957747154594767; +struct Vertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, +} + +@vertex +fn vertex(vertex: Vertex) -> @builtin(position) vec4 { + return volumetric_fog.clip_from_local * vec4(vertex.position, 1.0); +} + // The common Henyey-Greenstein asymmetric phase function [1] [2]. // // This determines how much light goes toward the viewer as opposed to away from @@ -68,80 +91,114 @@ fn henyey_greenstein(neg_LdotV: f32) -> f32 { } @fragment -fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { +fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { // Unpack the `volumetric_fog` settings. + let uvw_from_world = volumetric_fog.uvw_from_world; let fog_color = volumetric_fog.fog_color; let ambient_color = volumetric_fog.ambient_color; let ambient_intensity = volumetric_fog.ambient_intensity; let step_count = volumetric_fog.step_count; - let max_depth = volumetric_fog.max_depth; + let bounding_radius = volumetric_fog.bounding_radius; let absorption = volumetric_fog.absorption; let scattering = volumetric_fog.scattering; - let density = volumetric_fog.density; + let density_factor = volumetric_fog.density_factor; + let density_texture_offset = volumetric_fog.density_texture_offset; let light_tint = volumetric_fog.light_tint; let light_intensity = volumetric_fog.light_intensity; + let jitter_strength = volumetric_fog.jitter_strength; + // Unpack the view. let exposure = view.exposure; - // Sample the depth. If this is multisample, just use sample 0; this is - // approximate but good enough. - let frag_coord = in.position; - let depth = textureLoad(depth_texture, vec2(frag_coord.xy), 0); + // Sample the depth to put an upper bound on the length of the ray (as we + // shouldn't trace through solid objects). If this is multisample, just use + // sample 0; this is approximate but good enough. + let frag_coord = position; + let ndc_end_depth_from_buffer = textureLoad(depth_texture, vec2(frag_coord.xy), 0); + let view_end_depth_from_buffer = -position_ndc_to_view( + frag_coord_to_ndc(vec4(position.xy, ndc_end_depth_from_buffer, 1.0))).z; + + // Calculate the start position of the ray. Since we're only rendering front + // faces of the AABB, this is the current fragment's depth. + let view_start_pos = position_ndc_to_view(frag_coord_to_ndc(frag_coord)); + + // Calculate the end position of the ray. This requires us to raytrace the + // three back faces of the AABB to find the one that our ray intersects. + var end_depth_view = 0.0; + for (var plane_index = 0; plane_index < 3; plane_index += 1) { + let plane = volumetric_fog.far_planes[plane_index]; + let other_plane_a = volumetric_fog.far_planes[(plane_index + 1) % 3]; + let other_plane_b = volumetric_fog.far_planes[(plane_index + 2) % 3]; + + // Calculate the intersection of the ray and the plane. The ray must + // intersect in front of us (t > 0). + let t = -plane.w / dot(plane.xyz, view_start_pos.xyz); + if (t < 0.0) { + continue; + } + let hit_pos = view_start_pos.xyz * t; + + // The intersection point must be in front of the other backfaces. + let other_sides = vec2( + dot(vec4(hit_pos, 1.0), other_plane_a) >= 0.0, + dot(vec4(hit_pos, 1.0), other_plane_b) >= 0.0 + ); + + // If those tests pass, we found our backface. + if (all(other_sides)) { + end_depth_view = -hit_pos.z; + break; + } + } // Starting at the end depth, which we got above, figure out how long the // ray we want to trace is and the length of each increment. - let end_depth = min( - max_depth, - -position_ndc_to_view(frag_coord_to_ndc(vec4(in.position.xy, depth, 1.0))).z - ); - let step_size = end_depth / f32(step_count); + end_depth_view = min(end_depth_view, view_end_depth_from_buffer); + + // We assume world and view have the same scale here. + let start_depth_view = -depth_ndc_to_view_z(frag_coord.z); + let ray_length_view = abs(end_depth_view - start_depth_view); + let inv_step_count = 1.0 / f32(step_count); + let step_size_world = ray_length_view * inv_step_count; let directional_light_count = lights.n_directional_lights; // Calculate the ray origin (`Ro`) and the ray direction (`Rd`) in NDC, // view, and world coordinates. - let Rd_ndc = vec3(frag_coord_to_ndc(in.position).xy, 1.0); + let Rd_ndc = vec3(frag_coord_to_ndc(position).xy, 1.0); let Rd_view = normalize(position_ndc_to_view(Rd_ndc)); - let Ro_world = view.world_position; - let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - Ro_world); + var Ro_world = position_view_to_world(view_start_pos.xyz); + let Rd_world = normalize(position_ndc_to_world(Rd_ndc) - view.world_position); + + // Offset by jitter. + let jitter = interleaved_gradient_noise(position.xy, globals.frame_count) * jitter_strength; + Ro_world += Rd_world * jitter; // Use Beer's law [1] [2] to calculate the maximum amount of light that each // directional light could contribute, and modulate that value by the light // tint and fog color. (The actual value will in turn be modulated by the // phase according to the Henyey-Greenstein formula.) // - // We use a bit of a hack here. Conceptually, directional lights are - // infinitely far away. But, if we modeled exactly that, then directional - // lights would never contribute any light to the fog, because an - // infinitely-far directional light combined with an infinite amount of fog - // would result in complete absorption of the light. So instead we pretend - // that the directional light is `max_depth` units away and do the - // calculation in those terms. Because the fake distance to the directional - // light is a constant, this lets us perform the calculation once up here - // instead of marching secondary rays toward the light during the - // raymarching step, which improves performance dramatically. - // // [1]: https://www.scratchapixel.com/lessons/3d-basic-rendering/volume-rendering-for-developers/intro-volume-rendering.html // // [2]: https://en.wikipedia.org/wiki/Beer%E2%80%93Lambert_law - let light_attenuation = exp(-density * max_depth * (absorption + scattering)); - let light_factors_per_step = fog_color * light_tint * light_attenuation * scattering * - density * step_size * light_intensity * exposure; // Use Beer's law again to accumulate the ambient light all along the path. - var accumulated_color = exp(-end_depth * (absorption + scattering)) * ambient_color * + var accumulated_color = exp(-ray_length_view * (absorption + scattering)) * ambient_color * ambient_intensity; - // Pre-calculate absorption (amount of light absorbed by the fog) and - // out-scattering (amount of light the fog scattered away). This is the same - // amount for every step. - let sample_attenuation = exp(-step_size * density * (absorption + scattering)); - // This is the amount of the background that shows through. We're actually // going to recompute this over and over again for each directional light, // coming up with the same values each time. var background_alpha = 1.0; + // If we have a density texture, transform to its local space. +#ifdef DENSITY_TEXTURE + let Ro_uvw = (uvw_from_world * vec4(Ro_world, 1.0)).xyz; + let Rd_step_uvw = mat3x3(uvw_from_world[0].xyz, uvw_from_world[1].xyz, uvw_from_world[2].xyz) * + (Rd_world * step_size_world); +#endif // DENSITY_TEXTURE + for (var light_index = 0u; light_index < directional_light_count; light_index += 1u) { // Volumetric lights are all sorted first, so the first time we come to // a non-volumetric light, we know we've seen them all. @@ -158,10 +215,6 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { let neg_LdotV = dot(normalize((*light).direction_to_light.xyz), Rd_world); let phase = henyey_greenstein(neg_LdotV); - // Modulate the factor we calculated above by the phase, fog color, - // light color, light tint. - let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step; - // Reset `background_alpha` for a new raymarch. background_alpha = 1.0; @@ -173,8 +226,27 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { } // Calculate where we are in the ray. - let P_world = Ro_world + Rd_world * f32(step) * step_size; - let P_view = Rd_view * f32(step) * step_size; + let P_world = Ro_world + Rd_world * f32(step) * step_size_world; + let P_view = Rd_view * f32(step) * step_size_world; + + var density = density_factor; +#ifdef DENSITY_TEXTURE + // Take the density texture into account, if there is one. + // + // The uvs should never go outside the (0, 0, 0) to (1, 1, 1) box, + // but sometimes due to floating point error they can. Handle this + // case. + let P_uvw = Ro_uvw + Rd_step_uvw * f32(step); + if (all(P_uvw >= vec3(0.0)) && all(P_uvw <= vec3(1.0))) { + density *= textureSample(density_texture, density_sampler, P_uvw + density_texture_offset).r; + } else { + density = 0.0; + } +#endif // DENSITY_TEXTURE + + // Calculate absorption (amount of light absorbed by the fog) and + // out-scattering (amount of light the fog scattered away). + let sample_attenuation = exp(-step_size_world * density * (absorption + scattering)); // Process absorption and out-scattering. background_alpha *= sample_attenuation; @@ -205,6 +277,14 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { } if (local_light_attenuation != 0.0) { + let light_attenuation = exp(-density * bounding_radius * (absorption + scattering)); + let light_factors_per_step = fog_color * light_tint * light_attenuation * + scattering * density * step_size_world * light_intensity * exposure; + + // Modulate the factor we calculated above by the phase, fog color, + // light color, light tint. + let light_color_per_step = (*light).color.rgb * phase * light_factors_per_step; + // Accumulate the light. accumulated_color += light_color_per_step * local_light_attenuation * background_alpha; @@ -212,7 +292,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { } } - // We're done! Blend between the source color and the lit fog color. - let source = textureSample(color_texture, color_sampler, in.uv); - return vec4(source.rgb * background_alpha + accumulated_color, source.a); + // We're done! Return the color with alpha so it can be blended onto the + // render target. + return vec4(accumulated_color, 1.0 - background_alpha); } diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index b7aa3ea34e1ee3..eadd869e2d313c 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -9,7 +9,10 @@ license = "MIT OR Apache-2.0" [dependencies] bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } @@ -17,11 +20,14 @@ bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } + uuid = { version = "1.1", features = ["v4"] } [lints] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options"] all-features = true diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs new file mode 100644 index 00000000000000..092fa6ac264b70 --- /dev/null +++ b/crates/bevy_picking/src/events.rs @@ -0,0 +1,672 @@ +//! Processes data from input and backends, producing interaction events. + +use std::fmt::Debug; + +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::*; +use bevy_hierarchy::Parent; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; +use bevy_utils::{tracing::debug, Duration, HashMap, Instant}; + +use crate::{ + backend::{prelude::PointerLocation, HitData}, + focus::{HoverMap, PreviousHoverMap}, + pointer::{ + InputMove, InputPress, Location, PointerButton, PointerId, PointerMap, PressDirection, + }, +}; + +/// Stores the common data needed for all `PointerEvent`s. +#[derive(Clone, PartialEq, Debug, Reflect, Component)] +pub struct Pointer { + /// The target of this event + pub target: Entity, + /// The pointer that triggered this event + pub pointer_id: PointerId, + /// The location of the pointer during this event + pub pointer_location: Location, + /// Additional event-specific data. [`Drop`] for example, has an additional field to describe + /// the `Entity` that is being dropped on the target. + pub event: E, +} + +impl Event for Pointer +where + E: Debug + Clone + Reflect, +{ + type Traversal = Parent; + const AUTO_PROPAGATE: bool = true; +} + +impl std::fmt::Display for Pointer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{:?}, {:.1?}, {:?}, {:.1?}", + self.pointer_id, self.pointer_location.position, self.target, self.event + )) + } +} + +impl std::ops::Deref for Pointer { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.event + } +} + +impl Pointer { + /// Construct a new `PointerEvent`. + pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { + Self { + pointer_id: id, + pointer_location: location, + target, + event, + } + } +} + +/// Fires when a pointer is no longer available. +#[derive(Event, Clone, PartialEq, Debug, Reflect)] +pub struct PointerCancel { + /// ID of the pointer that was cancelled. + #[reflect(ignore)] + pub pointer_id: PointerId, +} + +/// Fires when a the pointer crosses into the bounds of the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Over { + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Fires when a the pointer crosses out of the bounds of the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Out { + /// Information about the latest prior picking intersection. + pub hit: HitData, +} + +/// Fires when a pointer button is pressed over the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Down { + /// Pointer button pressed to trigger this event. + pub button: PointerButton, + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Fires when a pointer button is released over the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Up { + /// Pointer button lifted to trigger this event. + pub button: PointerButton, + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Fires when a pointer sends a pointer down event followed by a pointer up event, with the same +/// `target` entity for both events. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Click { + /// Pointer button pressed and lifted to trigger this event. + pub button: PointerButton, + /// Information about the picking intersection. + pub hit: HitData, + /// Duration between the pointer pressed and lifted for this click + pub duration: Duration, +} + +/// Fires while a pointer is moving over the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Move { + /// Information about the picking intersection. + pub hit: HitData, + /// The change in position since the last move event. + pub delta: Vec2, +} + +/// Fires when the `target` entity receives a pointer down event followed by a pointer move event. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct DragStart { + /// Pointer button pressed and moved to trigger this event. + pub button: PointerButton, + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Fires while the `target` entity is being dragged. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Drag { + /// Pointer button pressed and moved to trigger this event. + pub button: PointerButton, + /// The total distance vector of a drag, measured from drag start to the current position. + pub distance: Vec2, + /// The change in position since the last drag event. + pub delta: Vec2, +} + +/// Fires when a pointer is dragging the `target` entity and a pointer up event is received. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct DragEnd { + /// Pointer button pressed, moved, and lifted to trigger this event. + pub button: PointerButton, + /// The vector of drag movement measured from start to final pointer position. + pub distance: Vec2, +} + +/// Fires when a pointer dragging the `dragged` entity enters the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct DragEnter { + /// Pointer button pressed to enter drag. + pub button: PointerButton, + /// The entity that was being dragged when the pointer entered the `target` entity. + pub dragged: Entity, + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Fires while the `dragged` entity is being dragged over the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct DragOver { + /// Pointer button pressed while dragging over. + pub button: PointerButton, + /// The entity that was being dragged when the pointer was over the `target` entity. + pub dragged: Entity, + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Fires when a pointer dragging the `dragged` entity leaves the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct DragLeave { + /// Pointer button pressed while leaving drag. + pub button: PointerButton, + /// The entity that was being dragged when the pointer left the `target` entity. + pub dragged: Entity, + /// Information about the latest prior picking intersection. + pub hit: HitData, +} + +/// Fires when a pointer drops the `dropped` entity onto the `target` entity. +#[derive(Clone, PartialEq, Debug, Reflect)] +pub struct Drop { + /// Pointer button lifted to drop. + pub button: PointerButton, + /// The entity that was dropped onto the `target` entity. + pub dropped: Entity, + /// Information about the picking intersection. + pub hit: HitData, +} + +/// Generates pointer events from input and focus data +#[allow(clippy::too_many_arguments)] +pub fn pointer_events( + mut commands: Commands, + // Input + mut input_presses: EventReader, + mut input_moves: EventReader, + pointer_map: Res, + pointers: Query<&PointerLocation>, + hover_map: Res, + previous_hover_map: Res, + // Output + mut pointer_move: EventWriter>, + mut pointer_over: EventWriter>, + mut pointer_out: EventWriter>, + mut pointer_up: EventWriter>, + mut pointer_down: EventWriter>, +) { + let pointer_location = |pointer_id: PointerId| { + pointer_map + .get_entity(pointer_id) + .and_then(|entity| pointers.get(entity).ok()) + .and_then(|pointer| pointer.location.clone()) + }; + + for InputMove { + pointer_id, + location, + delta, + } in input_moves.read().cloned() + { + for (hovered_entity, hit) in hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) + { + let event = Pointer::new( + pointer_id, + location.clone(), + hovered_entity, + Move { hit, delta }, + ); + commands.trigger_targets(event.clone(), event.target); + pointer_move.send(event); + } + } + + for press_event in input_presses.read() { + let button = press_event.button; + // We use the previous hover map because we want to consider pointers that just left the + // entity. Without this, touch inputs would never send up events because they are lifted up + // and leave the bounds of the entity at the same time. + for (hovered_entity, hit) in previous_hover_map + .get(&press_event.pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) + { + if let PressDirection::Up = press_event.direction { + let Some(location) = pointer_location(press_event.pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during event {:?}", + press_event.pointer_id, press_event + ); + continue; + }; + let event = Pointer::new( + press_event.pointer_id, + location, + hovered_entity, + Up { button, hit }, + ); + commands.trigger_targets(event.clone(), event.target); + pointer_up.send(event); + } + } + for (hovered_entity, hit) in hover_map + .get(&press_event.pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.clone()))) + { + if let PressDirection::Down = press_event.direction { + let Some(location) = pointer_location(press_event.pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during event {:?}", + press_event.pointer_id, press_event + ); + continue; + }; + let event = Pointer::new( + press_event.pointer_id, + location, + hovered_entity, + Down { button, hit }, + ); + commands.trigger_targets(event.clone(), event.target); + pointer_down.send(event); + } + } + } + + // If the entity is hovered... + for (pointer_id, hovered_entity, hit) in hover_map + .iter() + .flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone()))) + { + // ...but was not hovered last frame... + if !previous_hover_map + .get(&pointer_id) + .iter() + .any(|e| e.contains_key(&hovered_entity)) + { + let Some(location) = pointer_location(pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during pointer over", + pointer_id + ); + continue; + }; + let event = Pointer::new(pointer_id, location, hovered_entity, Over { hit }); + commands.trigger_targets(event.clone(), event.target); + pointer_over.send(event); + } + } + + // If the entity was hovered by a specific pointer last frame... + for (pointer_id, hovered_entity, hit) in previous_hover_map + .iter() + .flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone()))) + { + // ...but is now not being hovered by that same pointer... + if !hover_map + .get(&pointer_id) + .iter() + .any(|e| e.contains_key(&hovered_entity)) + { + let Some(location) = pointer_location(pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during pointer out", + pointer_id + ); + continue; + }; + let event = Pointer::new(pointer_id, location, hovered_entity, Out { hit }); + commands.trigger_targets(event.clone(), event.target); + pointer_out.send(event); + } + } +} + +/// Maps pointers to the entities they are dragging. +#[derive(Debug, Deref, DerefMut, Default, Resource)] +pub struct DragMap(pub HashMap<(PointerId, PointerButton), HashMap>); + +/// An entry in the [`DragMap`]. +#[derive(Debug, Clone)] +pub struct DragEntry { + /// The position of the pointer at drag start. + pub start_pos: Vec2, + /// The latest position of the pointer during this drag, used to compute deltas. + pub latest_pos: Vec2, +} + +/// Uses pointer events to determine when click and drag events occur. +#[allow(clippy::too_many_arguments)] +pub fn send_click_and_drag_events( + // for triggering observers + // - Pointer + // - Pointer + // - Pointer + mut commands: Commands, + // Input + mut pointer_down: EventReader>, + mut pointer_up: EventReader>, + mut input_move: EventReader, + mut input_presses: EventReader, + pointer_map: Res, + pointers: Query<&PointerLocation>, + // Locals + mut down_map: Local< + HashMap<(PointerId, PointerButton), HashMap, Instant)>>, + >, + // Outputs used for further processing + mut drag_map: ResMut, + mut pointer_drag_end: EventWriter>, +) { + let pointer_location = |pointer_id: PointerId| { + pointer_map + .get_entity(pointer_id) + .and_then(|entity| pointers.get(entity).ok()) + .and_then(|pointer| pointer.location.clone()) + }; + + // Triggers during movement even if not over an entity + for InputMove { + pointer_id, + location, + delta: _, + } in input_move.read().cloned() + { + for button in PointerButton::iter() { + let Some(down_list) = down_map.get(&(pointer_id, button)) else { + continue; + }; + let drag_list = drag_map.entry((pointer_id, button)).or_default(); + + for (down, _instant) in down_list.values() { + if drag_list.contains_key(&down.target) { + continue; // this entity is already logged as being dragged + } + drag_list.insert( + down.target, + DragEntry { + start_pos: down.pointer_location.position, + latest_pos: down.pointer_location.position, + }, + ); + let event = Pointer::new( + pointer_id, + down.pointer_location.clone(), + down.target, + DragStart { + button, + hit: down.hit.clone(), + }, + ); + commands.trigger_targets(event, down.target); + } + + for (dragged_entity, drag) in drag_list.iter_mut() { + let drag_event = Drag { + button, + distance: location.position - drag.start_pos, + delta: location.position - drag.latest_pos, + }; + drag.latest_pos = location.position; + let target = *dragged_entity; + let event = Pointer::new(pointer_id, location.clone(), target, drag_event); + commands.trigger_targets(event, target); + } + } + } + + // Triggers when button is released over an entity + let now = Instant::now(); + for Pointer { + pointer_id, + pointer_location, + target, + event: Up { button, hit }, + } in pointer_up.read().cloned() + { + // Can't have a click without the button being pressed down first + if let Some((_down, down_instant)) = down_map + .get(&(pointer_id, button)) + .and_then(|down| down.get(&target)) + { + let duration = now - *down_instant; + let event = Pointer::new( + pointer_id, + pointer_location, + target, + Click { + button, + hit, + duration, + }, + ); + commands.trigger_targets(event, target); + } + } + + // Triggers when button is pressed over an entity + for event in pointer_down.read() { + let button = event.button; + let down_button_entity_map = down_map.entry((event.pointer_id, button)).or_default(); + down_button_entity_map.insert(event.target, (event.clone(), now)); + } + + // Triggered for all button presses + for press in input_presses.read() { + if press.direction != PressDirection::Up { + continue; // We are only interested in button releases + } + down_map.insert((press.pointer_id, press.button), HashMap::new()); + let Some(drag_list) = drag_map.insert((press.pointer_id, press.button), HashMap::new()) + else { + continue; + }; + let Some(location) = pointer_location(press.pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during event {:?}", + press.pointer_id, press + ); + continue; + }; + + for (drag_target, drag) in drag_list { + let drag_end = DragEnd { + button: press.button, + distance: drag.latest_pos - drag.start_pos, + }; + let event = Pointer::new(press.pointer_id, location.clone(), drag_target, drag_end); + commands.trigger_targets(event.clone(), event.target); + pointer_drag_end.send(event); + } + } +} + +/// Uses pointer events to determine when drag-over events occur +#[allow(clippy::too_many_arguments)] +pub fn send_drag_over_events( + // uses this to trigger the following + // - Pointer, + // - Pointer, + // - Pointer, + // - Pointer, + mut commands: Commands, + // Input + drag_map: Res, + mut pointer_over: EventReader>, + mut pointer_move: EventReader>, + mut pointer_out: EventReader>, + mut pointer_drag_end: EventReader>, + // Local + mut drag_over_map: Local>>, +) { + // Fire PointerDragEnter events. + for Pointer { + pointer_id, + pointer_location, + target, + event: Over { hit }, + } in pointer_over.read().cloned() + { + for button in PointerButton::iter() { + for drag_target in drag_map + .get(&(pointer_id, button)) + .iter() + .flat_map(|drag_list| drag_list.keys()) + .filter( + |&&drag_target| target != drag_target, /* can't drag over itself */ + ) + { + let drag_entry = drag_over_map.entry((pointer_id, button)).or_default(); + drag_entry.insert(target, hit.clone()); + let event = Pointer::new( + pointer_id, + pointer_location.clone(), + target, + DragEnter { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(event, target); + } + } + } + + // Fire PointerDragOver events. + for Pointer { + pointer_id, + pointer_location, + target, + event: Move { hit, delta: _ }, + } in pointer_move.read().cloned() + { + for button in PointerButton::iter() { + for drag_target in drag_map + .get(&(pointer_id, button)) + .iter() + .flat_map(|drag_list| drag_list.keys()) + .filter( + |&&drag_target| target != drag_target, /* can't drag over itself */ + ) + { + let event = Pointer::new( + pointer_id, + pointer_location.clone(), + target, + DragOver { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(event, target); + } + } + } + + // Fire PointerDragLeave and PointerDrop events when the pointer stops dragging. + for Pointer { + pointer_id, + pointer_location, + target: drag_end_target, + event: DragEnd { + button, + distance: _, + }, + } in pointer_drag_end.read().cloned() + { + let Some(drag_over_set) = drag_over_map.get_mut(&(pointer_id, button)) else { + continue; + }; + for (dragged_over, hit) in drag_over_set.drain() { + let target = dragged_over; + let event = Pointer::new( + pointer_id, + pointer_location.clone(), + dragged_over, + DragLeave { + button, + dragged: drag_end_target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(event, target); + + let event = Pointer::new( + pointer_id, + pointer_location.clone(), + target, + Drop { + button, + dropped: target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(event, target); + } + } + + // Fire PointerDragLeave events when the pointer goes out of the target. + for Pointer { + pointer_id, + pointer_location, + target, + event: Out { hit }, + } in pointer_out.read().cloned() + { + for button in PointerButton::iter() { + let Some(dragged_over) = drag_over_map.get_mut(&(pointer_id, button)) else { + continue; + }; + if dragged_over.remove(&target).is_none() { + continue; + } + let Some(drag_list) = drag_map.get(&(pointer_id, button)) else { + continue; + }; + for drag_target in drag_list.keys() { + let event = Pointer::new( + pointer_id, + pointer_location.clone(), + target, + DragLeave { + button, + dragged: *drag_target, + hit: hit.clone(), + }, + ); + commands.trigger_targets(event, target); + } + } + } +} diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs new file mode 100644 index 00000000000000..8a593335cdc141 --- /dev/null +++ b/crates/bevy_picking/src/focus.rs @@ -0,0 +1,266 @@ +//! Determines which entities are being hovered by which pointers. + +use std::{collections::BTreeMap, fmt::Debug}; + +use crate::{ + backend::{self, HitData}, + events::PointerCancel, + pointer::{PointerId, PointerInteraction, PointerPress}, + Pickable, +}; + +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::*; +use bevy_math::FloatOrd; +use bevy_reflect::prelude::*; +use bevy_utils::HashMap; + +type DepthSortedHits = Vec<(Entity, HitData)>; + +/// Events returned from backends can be grouped with an order field. This allows picking to work +/// with multiple layers of rendered output to the same render target. +type PickLayer = FloatOrd; + +/// Maps [`PickLayer`]s to the map of entities within that pick layer, sorted by depth. +type LayerMap = BTreeMap; + +/// Maps Pointers to a [`LayerMap`]. Note this is much more complex than the [`HoverMap`] because +/// this data structure is used to sort entities by layer then depth for every pointer. +type OverMap = HashMap; + +/// The source of truth for all hover state. This is used to determine what events to send, and what +/// state components should be in. +/// +/// Maps pointers to the entities they are hovering over. +/// +/// "Hovering" refers to the *hover* state, which is not the same as whether or not a picking +/// backend is reporting hits between a pointer and an entity. A pointer is "hovering" an entity +/// only if the pointer is hitting the entity (as reported by a picking backend) *and* no entities +/// between it and the pointer block interactions. +/// +/// For example, if a pointer is hitting a UI button and a 3d mesh, but the button is in front of +/// the mesh, and [`Pickable::should_block_lower`], the UI button will be hovered, but the mesh will +/// not. +/// +/// # Advanced Users +/// +/// If you want to completely replace the provided picking events or state produced by this plugin, +/// you can use this resource to do that. All of the event systems for picking are built *on top of* +/// this authoritative hover state, and you can do the same. You can also use the +/// [`PreviousHoverMap`] as a robust way of determining changes in hover state from the previous +/// update. +#[derive(Debug, Deref, DerefMut, Default, Resource)] +pub struct HoverMap(pub HashMap>); + +/// The previous state of the hover map, used to track changes to hover state. +#[derive(Debug, Deref, DerefMut, Default, Resource)] +pub struct PreviousHoverMap(pub HashMap>); + +/// Coalesces all data from inputs and backends to generate a map of the currently hovered entities. +/// This is the final focusing step to determine which entity the pointer is hovering over. +pub fn update_focus( + // Inputs + pickable: Query<&Pickable>, + pointers: Query<&PointerId>, + mut under_pointer: EventReader, + mut cancellations: EventReader, + // Local + mut over_map: Local, + // Output + mut hover_map: ResMut, + mut previous_hover_map: ResMut, +) { + reset_maps( + &mut hover_map, + &mut previous_hover_map, + &mut over_map, + &pointers, + ); + build_over_map(&mut under_pointer, &mut over_map, &mut cancellations); + build_hover_map(&pointers, pickable, &over_map, &mut hover_map); +} + +/// Clear non-empty local maps, reusing allocated memory. +fn reset_maps( + hover_map: &mut HoverMap, + previous_hover_map: &mut PreviousHoverMap, + over_map: &mut OverMap, + pointers: &Query<&PointerId>, +) { + // Swap the previous and current hover maps. This results in the previous values being stored in + // `PreviousHoverMap`. Swapping is okay because we clear the `HoverMap` which now holds stale + // data. This process is done without any allocations. + core::mem::swap(&mut previous_hover_map.0, &mut hover_map.0); + + for entity_set in hover_map.values_mut() { + entity_set.clear(); + } + for layer_map in over_map.values_mut() { + layer_map.clear(); + } + + // Clear pointers from the maps if they have been removed. + let active_pointers: Vec = pointers.iter().copied().collect(); + hover_map.retain(|pointer, _| active_pointers.contains(pointer)); + over_map.retain(|pointer, _| active_pointers.contains(pointer)); +} + +/// Build an ordered map of entities that are under each pointer +fn build_over_map( + backend_events: &mut EventReader, + pointer_over_map: &mut Local, + pointer_cancel: &mut EventReader, +) { + let cancelled_pointers: Vec = pointer_cancel.read().map(|p| p.pointer_id).collect(); + + for entities_under_pointer in backend_events + .read() + .filter(|e| !cancelled_pointers.contains(&e.pointer)) + { + let pointer = entities_under_pointer.pointer; + let layer_map = pointer_over_map + .entry(pointer) + .or_insert_with(BTreeMap::new); + for (entity, pick_data) in entities_under_pointer.picks.iter() { + let layer = entities_under_pointer.order; + let hits = layer_map.entry(FloatOrd(layer)).or_insert_with(Vec::new); + hits.push((*entity, pick_data.clone())); + } + } + + for layers in pointer_over_map.values_mut() { + for hits in layers.values_mut() { + hits.sort_by_key(|(_, hit)| FloatOrd(hit.depth)); + } + } +} + +/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`Pickable`]. Note +/// that unlike the pointer map, this uses [`Pickable`] to determine if lower entities receive hover +/// focus. Often, only a single entity per pointer will be hovered. +fn build_hover_map( + pointers: &Query<&PointerId>, + pickable: Query<&Pickable>, + over_map: &Local, + // Output + hover_map: &mut HoverMap, +) { + for pointer_id in pointers.iter() { + let pointer_entity_set = hover_map.entry(*pointer_id).or_insert_with(HashMap::new); + if let Some(layer_map) = over_map.get(pointer_id) { + // Note we reverse here to start from the highest layer first. + for (entity, pick_data) in layer_map.values().rev().flatten() { + if let Ok(pickable) = pickable.get(*entity) { + if pickable.is_hoverable { + pointer_entity_set.insert(*entity, pick_data.clone()); + } + if pickable.should_block_lower { + break; + } + } else { + pointer_entity_set.insert(*entity, pick_data.clone()); // Emit events by default + break; // Entities block by default so we break out of the loop + } + } + } + } +} + +/// A component that aggregates picking interaction state of this entity across all pointers. +/// +/// Unlike bevy's `Interaction` component, this is an aggregate of the state of all pointers +/// interacting with this entity. Aggregation is done by taking the interaction with the highest +/// precedence. +/// +/// For example, if we have an entity that is being hovered by one pointer, and pressed by another, +/// the entity will be considered pressed. If that entity is instead being hovered by both pointers, +/// it will be considered hovered. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default)] +pub enum PickingInteraction { + /// The entity is being pressed down by a pointer. + Pressed = 2, + /// The entity is being hovered by a pointer. + Hovered = 1, + /// No pointers are interacting with this entity. + #[default] + None = 0, +} + +/// Uses pointer events to update [`PointerInteraction`] and [`PickingInteraction`] components. +pub fn update_interactions( + // Input + hover_map: Res, + previous_hover_map: Res, + // Outputs + mut commands: Commands, + mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, + mut interact: Query<&mut PickingInteraction>, +) { + // Clear all previous hover data from pointers and entities + for (pointer, _, mut pointer_interaction) in &mut pointers { + pointer_interaction.sorted_entities.clear(); + if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) { + for entity in previously_hovered_entities.keys() { + if let Ok(mut interaction) = interact.get_mut(*entity) { + *interaction = PickingInteraction::None; + } + } + } + } + + // Create a map to hold the aggregated interaction for each entity. This is needed because we + // need to be able to insert the interaction component on entities if they do not exist. To do + // so we need to know the final aggregated interaction state to avoid the scenario where we set + // an entity to `Pressed`, then overwrite that with a lower precedent like `Hovered`. + let mut new_interaction_state = HashMap::::new(); + for (pointer, pointer_press, mut pointer_interaction) in &mut pointers { + if let Some(pointers_hovered_entities) = hover_map.get(pointer) { + // Insert a sorted list of hit entities into the pointer's interaction component. + let mut sorted_entities: Vec<_> = pointers_hovered_entities.clone().drain().collect(); + sorted_entities.sort_by_key(|(_entity, hit)| FloatOrd(hit.depth)); + pointer_interaction.sorted_entities = sorted_entities; + + for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) { + merge_interaction_states(pointer_press, hovered_entity, &mut new_interaction_state); + } + } + } + + // Take the aggregated entity states and update or insert the component if missing. + for (hovered_entity, new_interaction) in new_interaction_state.drain() { + if let Ok(mut interaction) = interact.get_mut(hovered_entity) { + *interaction = new_interaction; + } else if let Some(mut entity_commands) = commands.get_entity(hovered_entity) { + entity_commands.try_insert(new_interaction); + } + } +} + +/// Merge the interaction state of this entity into the aggregated map. +fn merge_interaction_states( + pointer_press: &PointerPress, + hovered_entity: &Entity, + new_interaction_state: &mut HashMap, +) { + let new_interaction = match pointer_press.is_any_pressed() { + true => PickingInteraction::Pressed, + false => PickingInteraction::Hovered, + }; + + if let Some(old_interaction) = new_interaction_state.get_mut(hovered_entity) { + // Only update if the new value has a higher precedence than the old value. + if *old_interaction != new_interaction + && matches!( + (*old_interaction, new_interaction), + (PickingInteraction::Hovered, PickingInteraction::Pressed) + | (PickingInteraction::None, PickingInteraction::Pressed) + | (PickingInteraction::None, PickingInteraction::Hovered) + ) + { + *old_interaction = new_interaction; + } + } else { + new_interaction_state.insert(*hovered_entity, new_interaction); + } +} diff --git a/crates/bevy_picking/src/input/mod.rs b/crates/bevy_picking/src/input/mod.rs new file mode 100644 index 00000000000000..e3dd9e3695e6dc --- /dev/null +++ b/crates/bevy_picking/src/input/mod.rs @@ -0,0 +1,86 @@ +//! `bevy_picking::input` is a thin layer that provides unsurprising default inputs to `bevy_picking`. +//! The included systems are responsible for sending mouse and touch inputs to their +//! respective `Pointer`s. +//! +//! Because this resides in its own crate, it's easy to omit it, and provide your own inputs as +//! needed. Because `Pointer`s aren't coupled to the underlying input hardware, you can easily mock +//! inputs, and allow users full accessibility to map whatever inputs they need to pointer input. +//! +//! If, for example, you wanted to add support for VR input, all you need to do is spawn a pointer +//! entity with a custom [`PointerId`](crate::pointer::PointerId), and write a system +//! that updates its position. +//! +//! TODO: Update docs + +use bevy_app::prelude::*; +use bevy_ecs::prelude::*; +use bevy_reflect::prelude::*; + +use crate::PickSet; + +pub mod mouse; +pub mod touch; + +/// Common imports for `bevy_picking_input`. +pub mod prelude { + pub use crate::input::InputPlugin; +} + +/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin, +/// that you can replace with your own plugin as needed. +/// +/// [`crate::PickingPluginsSettings::is_input_enabled`] can be used to toggle whether +/// the core picking plugin processes the inputs sent by this, or other input plugins, in one place. +#[derive(Copy, Clone, Resource, Debug, Reflect)] +#[reflect(Resource, Default)] +pub struct InputPlugin { + /// Should touch inputs be updated? + pub is_touch_enabled: bool, + /// Should mouse inputs be updated? + pub is_mouse_enabled: bool, +} + +impl InputPlugin { + fn is_mouse_enabled(state: Res) -> bool { + state.is_mouse_enabled + } + + fn is_touch_enabled(state: Res) -> bool { + state.is_touch_enabled + } +} + +impl Default for InputPlugin { + fn default() -> Self { + Self { + is_touch_enabled: true, + is_mouse_enabled: true, + } + } +} + +impl Plugin for InputPlugin { + fn build(&self, app: &mut App) { + app.insert_resource(*self) + .add_systems(Startup, mouse::spawn_mouse_pointer) + .add_systems( + First, + ( + mouse::mouse_pick_events.run_if(InputPlugin::is_mouse_enabled), + touch::touch_pick_events.run_if(InputPlugin::is_touch_enabled), + // IMPORTANT: the commands must be flushed after `touch_pick_events` is run + // because we need pointer spawning to happen immediately to prevent issues with + // missed events during drag and drop. + apply_deferred, + ) + .chain() + .in_set(PickSet::Input), + ) + .add_systems( + Last, + touch::deactivate_touch_pointers.run_if(InputPlugin::is_touch_enabled), + ) + .register_type::() + .register_type::(); + } +} diff --git a/crates/bevy_picking/src/input/mouse.rs b/crates/bevy_picking/src/input/mouse.rs new file mode 100644 index 00000000000000..73cf321f611650 --- /dev/null +++ b/crates/bevy_picking/src/input/mouse.rs @@ -0,0 +1,67 @@ +//! Provides sensible defaults for mouse picking inputs. + +use bevy_ecs::prelude::*; +use bevy_input::{mouse::MouseButtonInput, prelude::*, ButtonState}; +use bevy_math::Vec2; +use bevy_render::camera::RenderTarget; +use bevy_window::{CursorMoved, PrimaryWindow, Window, WindowRef}; + +use crate::{ + pointer::{InputMove, InputPress, Location, PointerButton, PointerId}, + PointerBundle, +}; + +/// Spawns the default mouse pointer. +pub fn spawn_mouse_pointer(mut commands: Commands) { + commands.spawn((PointerBundle::new(PointerId::Mouse),)); +} + +/// Sends mouse pointer events to be processed by the core plugin +pub fn mouse_pick_events( + // Input + windows: Query<(Entity, &Window), With>, + mut cursor_moves: EventReader, + mut cursor_last: Local, + mut mouse_inputs: EventReader, + // Output + mut pointer_move: EventWriter, + mut pointer_presses: EventWriter, +) { + for event in cursor_moves.read() { + pointer_move.send(InputMove::new( + PointerId::Mouse, + Location { + target: RenderTarget::Window(WindowRef::Entity(event.window)) + .normalize(Some( + match windows.get_single() { + Ok(w) => w, + Err(_) => continue, + } + .0, + )) + .unwrap(), + position: event.position, + }, + event.position - *cursor_last, + )); + *cursor_last = event.position; + } + + for input in mouse_inputs.read() { + let button = match input.button { + MouseButton::Left => PointerButton::Primary, + MouseButton::Right => PointerButton::Secondary, + MouseButton::Middle => PointerButton::Middle, + MouseButton::Other(_) | MouseButton::Back | MouseButton::Forward => continue, + }; + + match input.state { + ButtonState::Pressed => { + pointer_presses.send(InputPress::new_down(PointerId::Mouse, button)); + } + ButtonState::Released => { + pointer_presses.send(InputPress::new_up(PointerId::Mouse, button)); + } + } + } +} diff --git a/crates/bevy_picking/src/input/touch.rs b/crates/bevy_picking/src/input/touch.rs new file mode 100644 index 00000000000000..b6b7e6a33c85c4 --- /dev/null +++ b/crates/bevy_picking/src/input/touch.rs @@ -0,0 +1,105 @@ +//! Provides sensible defaults for touch picking inputs. + +use bevy_ecs::prelude::*; +use bevy_hierarchy::DespawnRecursiveExt; +use bevy_input::touch::{TouchInput, TouchPhase}; +use bevy_math::Vec2; +use bevy_render::camera::RenderTarget; +use bevy_utils::{tracing::debug, HashMap, HashSet}; +use bevy_window::{PrimaryWindow, WindowRef}; + +use crate::{ + events::PointerCancel, + pointer::{InputMove, InputPress, Location, PointerButton, PointerId}, + PointerBundle, +}; + +/// Sends touch pointer events to be consumed by the core plugin +/// +/// IMPORTANT: the commands must be flushed after this system is run because we need spawning to +/// happen immediately to prevent issues with missed events needed for drag and drop. +pub fn touch_pick_events( + // Input + mut touches: EventReader, + primary_window: Query>, + // Local + mut location_cache: Local>, + // Output + mut commands: Commands, + mut input_moves: EventWriter, + mut input_presses: EventWriter, + mut cancel_events: EventWriter, +) { + for touch in touches.read() { + let pointer = PointerId::Touch(touch.id); + let location = Location { + target: match RenderTarget::Window(WindowRef::Entity(touch.window)) + .normalize(primary_window.get_single().ok()) + { + Some(target) => target, + None => continue, + }, + position: touch.position, + }; + match touch.phase { + TouchPhase::Started => { + debug!("Spawning pointer {:?}", pointer); + commands.spawn((PointerBundle::new(pointer).with_location(location.clone()),)); + + input_moves.send(InputMove::new(pointer, location, Vec2::ZERO)); + input_presses.send(InputPress::new_down(pointer, PointerButton::Primary)); + location_cache.insert(touch.id, *touch); + } + TouchPhase::Moved => { + // Send a move event only if it isn't the same as the last one + if let Some(last_touch) = location_cache.get(&touch.id) { + if last_touch == touch { + continue; + } + input_moves.send(InputMove::new( + pointer, + location, + touch.position - last_touch.position, + )); + } + location_cache.insert(touch.id, *touch); + } + TouchPhase::Ended | TouchPhase::Canceled => { + input_presses.send(InputPress::new_up(pointer, PointerButton::Primary)); + location_cache.remove(&touch.id); + cancel_events.send(PointerCancel { + pointer_id: pointer, + }); + } + } + } +} + +/// Deactivates unused touch pointers. +/// +/// Because each new touch gets assigned a new ID, we need to remove the pointers associated with +/// touches that are no longer active. +pub fn deactivate_touch_pointers( + mut commands: Commands, + mut despawn_list: Local>, + pointers: Query<(Entity, &PointerId)>, + mut touches: EventReader, +) { + for touch in touches.read() { + match touch.phase { + TouchPhase::Ended | TouchPhase::Canceled => { + for (entity, pointer) in &pointers { + if pointer.get_touch_id() == Some(touch.id) { + despawn_list.insert((entity, *pointer)); + } + } + } + _ => {} + } + } + // A hash set is used to prevent despawning the same entity twice. + for (entity, pointer) in despawn_list.drain() { + debug!("Despawning pointer {:?}", pointer); + commands.entity(entity).despawn_recursive(); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 4063b34d7121b2..ba0ef8eee98faf 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -3,12 +3,24 @@ #![deny(missing_docs)] pub mod backend; +pub mod events; +pub mod focus; +pub mod input; pub mod pointer; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +/// common exports for picking interaction +pub mod prelude { + #[doc(hidden)] + pub use crate::{ + events::*, input::InputPlugin, pointer::PointerButton, DefaultPickingPlugins, + InteractionPlugin, Pickable, PickingPlugin, PickingPluginsSettings, + }; +} + /// Used to globally toggle picking features at runtime. #[derive(Clone, Debug, Resource, Reflect)] #[reflect(Resource, Default)] @@ -26,8 +38,6 @@ impl PickingPluginsSettings { pub fn input_should_run(state: Res) -> bool { state.is_input_enabled && state.is_enabled } - // TODO: remove this allow after focus/hover is implemented in bevy_picking - #[allow(rustdoc::broken_intra_doc_links)] /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) /// component should be running. pub fn focus_should_run(state: Res) -> bool { @@ -72,11 +82,7 @@ pub struct Pickable { /// /// Entities without the [`Pickable`] component will block by default. pub should_block_lower: bool, - // TODO: remove this allow after focus/hover is implemented in bevy_picking - #[allow(rustdoc::broken_intra_doc_links)] - /// Should this entity be added to the [`HoverMap`](focus::HoverMap) and thus emit events when - /// targeted? - /// + /// If this is set to `false` and `should_block_lower` is set to true, this entity will block /// lower entities from being interacted and at the same time will itself not emit any events. /// @@ -170,8 +176,27 @@ pub enum PickSet { Last, } +/// One plugin that contains the [`input::InputPlugin`], [`PickingPlugin`] and the [`InteractionPlugin`], +/// this is probably the plugin that will be most used. +/// Note: for any of these plugins to work, they require a picking backend to be active, +/// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] +/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. +#[derive(Default)] +pub struct DefaultPickingPlugins; + +impl Plugin for DefaultPickingPlugins { + fn build(&self, app: &mut App) { + app.add_plugins(( + input::InputPlugin::default(), + PickingPlugin, + InteractionPlugin, + )); + } +} + /// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared /// types used by other picking plugins. +#[derive(Default)] pub struct PickingPlugin; impl Plugin for PickingPlugin { @@ -188,11 +213,18 @@ impl Plugin for PickingPlugin { pointer::update_pointer_map, pointer::InputMove::receive, pointer::InputPress::receive, - backend::ray::RayMap::repopulate, + backend::ray::RayMap::repopulate.after(pointer::InputMove::receive), ) .in_set(PickSet::ProcessInput), ) - .configure_sets(First, (PickSet::Input, PickSet::PostInput).chain()) + .configure_sets( + First, + (PickSet::Input, PickSet::PostInput) + .after(bevy_time::TimeSystem) + .ambiguous_with(bevy_asset::handle_internal_asset_events) + .after(bevy_ecs::event::EventUpdates) + .chain(), + ) .configure_sets( PreUpdate, ( @@ -203,6 +235,7 @@ impl Plugin for PickingPlugin { // Eventually events will need to be dispatched here PickSet::Last, ) + .ambiguous_with(bevy_asset::handle_internal_asset_events) .chain(), ) .register_type::() @@ -214,3 +247,37 @@ impl Plugin for PickingPlugin { .register_type::(); } } + +/// Generates [`Pointer`](events::Pointer) events and handles event bubbling. +#[derive(Default)] +pub struct InteractionPlugin; + +impl Plugin for InteractionPlugin { + fn build(&self, app: &mut App) { + use events::*; + use focus::{update_focus, update_interactions}; + + app.init_resource::() + .init_resource::() + .init_resource::() + .add_event::() + .add_event::>() + .add_event::>() + .add_event::>() + .add_event::>() + .add_event::>() + .add_event::>() + .add_systems( + PreUpdate, + ( + update_focus, + pointer_events, + update_interactions, + send_click_and_drag_events, + send_drag_over_events, + ) + .chain() + .in_set(PickSet::Focus), + ); + } +} diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index 428da4f9310704..aa95d6561ebbfa 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -14,5 +14,5 @@ keywords = ["bevy", "no_std"] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 51be4dff1e806b..24ea6022962565 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -7,9 +7,13 @@ html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -use core::fmt::{self, Formatter, Pointer}; use core::{ - cell::UnsafeCell, marker::PhantomData, mem::ManuallyDrop, num::NonZeroUsize, ptr::NonNull, + cell::UnsafeCell, + fmt::{self, Formatter, Pointer}, + marker::PhantomData, + mem::{align_of, ManuallyDrop}, + num::NonZeroUsize, + ptr::NonNull, }; /// Used as a type argument to [`Ptr`], [`PtrMut`] and [`OwningPtr`] to specify that the pointer is aligned. @@ -314,7 +318,7 @@ impl<'a, A: IsAligned> Ptr<'a, A> { } } -impl<'a, T> From<&'a T> for Ptr<'a> { +impl<'a, T: ?Sized> From<&'a T> for Ptr<'a> { #[inline] fn from(val: &'a T) -> Self { // SAFETY: The returned pointer has the same lifetime as the passed reference. @@ -384,7 +388,7 @@ impl<'a, A: IsAligned> PtrMut<'a, A> { } } -impl<'a, T> From<&'a mut T> for PtrMut<'a> { +impl<'a, T: ?Sized> From<&'a mut T> for PtrMut<'a> { #[inline] fn from(val: &'a mut T) -> Self { // SAFETY: The returned pointer has the same lifetime as the passed reference. @@ -599,7 +603,7 @@ trait DebugEnsureAligned { impl DebugEnsureAligned for *mut T { #[track_caller] fn debug_ensure_aligned(self) -> Self { - let align = core::mem::align_of::(); + let align = align_of::(); // Implementation shamelessly borrowed from the currently unstable // ptr.is_aligned_to. // diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 8cdd32204f741b..5cfbf833009fec 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -19,6 +19,8 @@ smallvec = ["dep:smallvec"] uuid = ["dep:uuid"] # When enabled, allows documentation comments to be accessed via reflection documentation = ["bevy_reflect_derive/documentation"] +# Enables function reflection +functions = ["bevy_reflect_derive/functions"] [dependencies] # bevy @@ -33,7 +35,7 @@ thiserror = "1.0" serde = "1" smallvec = { version = "1.11", optional = true } -glam = { version = "0.27", features = ["serde"], optional = true } +glam = { version = "0.28", features = ["serde"], optional = true } petgraph = { version = "0.6", features = ["serde-1"], optional = true } smol_str = { version = "0.2.0", optional = true } uuid = { version = "1.0", optional = true, features = ["v4", "serde"] } @@ -55,5 +57,5 @@ required-features = ["documentation"] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_reflect/README.md b/crates/bevy_reflect/README.md index 4289f869fff91c..8a145c7a67955f 100644 --- a/crates/bevy_reflect/README.md +++ b/crates/bevy_reflect/README.md @@ -8,7 +8,7 @@ This crate enables you to dynamically interact with Rust types: -* Derive the Reflect traits +* Derive the `Reflect` traits * Interact with fields using their names (for named structs) or indices (for tuple structs) * "Patch" your types with new values * Look up nested fields using "path strings" @@ -18,10 +18,10 @@ This crate enables you to dynamically interact with Rust types: ## Features -### Derive the Reflect traits +### Derive the `Reflect` traits ```rust ignore -// this will automatically implement the Reflect trait and the Struct trait (because the type is a struct) +// this will automatically implement the `Reflect` trait and the `Struct` trait (because the type is a struct) #[derive(Reflect)] struct Foo { a: u32, @@ -30,7 +30,7 @@ struct Foo { d: Vec, } -// this will automatically implement the Reflect trait and the TupleStruct trait (because the type is a tuple struct) +// this will automatically implement the `Reflect` trait and the `TupleStruct` trait (because the type is a tuple struct) #[derive(Reflect)] struct Bar(String); diff --git a/crates/bevy_reflect/compile_fail/Cargo.toml b/crates/bevy_reflect/compile_fail/Cargo.toml index dd1a6ef66c3049..14e5eb2264f826 100644 --- a/crates/bevy_reflect/compile_fail/Cargo.toml +++ b/crates/bevy_reflect/compile_fail/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT OR Apache-2.0" publish = false [dependencies] -bevy_reflect = { path = "../" } +bevy_reflect = { path = "../", features = ["functions"] } [dev-dependencies] compile_fail_utils = { path = "../../../tools/compile_fail_utils" } @@ -20,3 +20,7 @@ harness = false [[test]] name = "func" harness = false + +[[test]] +name = "remote" +harness = false diff --git a/crates/bevy_reflect/compile_fail/tests/derive.rs b/crates/bevy_reflect/compile_fail/tests/derive.rs index 1b1922254ceeda..6fc98526ee8997 100644 --- a/crates/bevy_reflect/compile_fail/tests/derive.rs +++ b/crates/bevy_reflect/compile_fail/tests/derive.rs @@ -1,3 +1,3 @@ fn main() -> compile_fail_utils::ui_test::Result<()> { - compile_fail_utils::test("tests/reflect_derive") + compile_fail_utils::test("reflect_derive", "tests/reflect_derive") } diff --git a/crates/bevy_reflect/compile_fail/tests/func.rs b/crates/bevy_reflect/compile_fail/tests/func.rs index 7a39bbc314d591..51ec8397a79729 100644 --- a/crates/bevy_reflect/compile_fail/tests/func.rs +++ b/crates/bevy_reflect/compile_fail/tests/func.rs @@ -1,3 +1,3 @@ fn main() -> compile_fail_utils::ui_test::Result<()> { - compile_fail_utils::test("tests/into_function") + compile_fail_utils::test("reflect_into_function", "tests/into_function") } diff --git a/crates/bevy_reflect/compile_fail/tests/into_function/arguments_fail.rs b/crates/bevy_reflect/compile_fail/tests/into_function/arguments_fail.rs index 27d7e89c571525..9187e874ec8239 100644 --- a/crates/bevy_reflect/compile_fail/tests/into_function/arguments_fail.rs +++ b/crates/bevy_reflect/compile_fail/tests/into_function/arguments_fail.rs @@ -33,8 +33,8 @@ fn main() { let _ = pass.into_function(); let _ = too_many_arguments.into_function(); - //~^ ERROR: no method named `into_function` found + //~^ E0599 let _ = argument_not_reflect.into_function(); - //~^ ERROR: no method named `into_function` found + //~^ E0599 } diff --git a/crates/bevy_reflect/compile_fail/tests/into_function/closure_fail.rs b/crates/bevy_reflect/compile_fail/tests/into_function/closure_fail.rs new file mode 100644 index 00000000000000..0083198534a1ea --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/into_function/closure_fail.rs @@ -0,0 +1,18 @@ +#![allow(unused)] + +use bevy_reflect::func::{DynamicFunction, IntoFunction}; +use bevy_reflect::Reflect; + +fn main() { + let value = String::from("Hello, World!"); + let closure_capture_owned = move || println!("{}", value); + + // Pass: + let _: DynamicFunction<'static> = closure_capture_owned.into_function(); + + let value = String::from("Hello, World!"); + let closure_capture_reference = || println!("{}", value); + //~^ ERROR: `value` does not live long enough + + let _: DynamicFunction<'static> = closure_capture_reference.into_function(); +} diff --git a/crates/bevy_reflect/compile_fail/tests/into_function/return_fail.rs b/crates/bevy_reflect/compile_fail/tests/into_function/return_fail.rs index a98322be91b183..d73a3406b306b2 100644 --- a/crates/bevy_reflect/compile_fail/tests/into_function/return_fail.rs +++ b/crates/bevy_reflect/compile_fail/tests/into_function/return_fail.rs @@ -25,10 +25,10 @@ fn main() { let _ = pass.into_function(); let _ = return_not_reflect.into_function(); - //~^ ERROR: no method named `into_function` found + //~^ E0599 let _ = return_with_lifetime_pass.into_function(); let _ = return_with_invalid_lifetime.into_function(); - //~^ ERROR: no method named `into_function` found + //~^ E0599 } diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.rs index ba463bf4dd322f..92b16288c407c1 100644 --- a/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.rs +++ b/crates/bevy_reflect/compile_fail/tests/reflect_derive/generics_fail.rs @@ -13,8 +13,9 @@ struct NoReflect(f32); fn main() { let mut foo: Box = Box::new(Foo:: { a: NoReflect(42.0) }); //~^ ERROR: `NoReflect` does not provide type registration information + //~| ERROR: `NoReflect` can not provide type information through reflection // foo doesn't implement Reflect because NoReflect doesn't implement Reflect foo.get_field::("a").unwrap(); - //~^ ERROR: `NoReflect` can not be reflected + //~^ ERROR: `NoReflect` does not implement `Reflect` so cannot be fully reflected } diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/incorrect_wrapper_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/incorrect_wrapper_fail.rs new file mode 100644 index 00000000000000..f25f3fd740f0cb --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/incorrect_wrapper_fail.rs @@ -0,0 +1,24 @@ +use bevy_reflect::Reflect; + +mod external_crate { + pub struct TheirFoo { + pub value: u32, + } +} + +#[repr(transparent)] +#[derive(Reflect)] +#[reflect(from_reflect = false)] +struct MyFoo(#[reflect(ignore)] pub external_crate::TheirFoo); + +#[derive(Reflect)] +//~^ ERROR: the trait bound `MyFoo: ReflectRemote` is not satisfied +#[reflect(from_reflect = false)] +struct MyStruct { + // Reason: `MyFoo` does not implement `ReflectRemote` (from `#[reflect_remote]` attribute) + #[reflect(remote = MyFoo)] + //~^ ERROR: the trait bound `MyFoo: ReflectRemote` is not satisfied + foo: external_crate::TheirFoo, +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/incorrect_wrapper_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/incorrect_wrapper_pass.rs new file mode 100644 index 00000000000000..798511a85aea26 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/incorrect_wrapper_pass.rs @@ -0,0 +1,21 @@ +//@check-pass +use bevy_reflect::{reflect_remote, Reflect}; + +mod external_crate { + pub struct TheirFoo { + pub value: u32, + } +} + +#[reflect_remote(external_crate::TheirFoo)] +struct MyFoo { + pub value: u32, +} + +#[derive(Reflect)] +struct MyStruct { + #[reflect(remote = MyFoo)] + foo: external_crate::TheirFoo, +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs new file mode 100644 index 00000000000000..d691c824ccac96 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_fail.rs @@ -0,0 +1,63 @@ +mod structs { + use bevy_reflect::reflect_remote; + + mod external_crate { + pub struct TheirStruct { + pub value: u32, + } + } + + #[reflect_remote(external_crate::TheirStruct)] + //~^ ERROR: `?` operator has incompatible types + struct MyStruct { + // Reason: Should be `u32` + pub value: bool, + //~^ ERROR: mismatched types + } +} + +mod tuple_structs { + use bevy_reflect::reflect_remote; + + mod external_crate { + pub struct TheirStruct(pub u32); + } + + #[reflect_remote(external_crate::TheirStruct)] + //~^ ERROR: `?` operator has incompatible types + struct MyStruct( + // Reason: Should be `u32` + pub bool, + //~^ ERROR: mismatched types + ); +} + +mod enums { + use bevy_reflect::reflect_remote; + + mod external_crate { + pub enum TheirStruct { + Unit, + Tuple(u32), + Struct { value: usize }, + } + } + + #[reflect_remote(external_crate::TheirStruct)] + //~^ ERROR: variant `enums::external_crate::TheirStruct::Unit` does not have a field named `0` + //~| ERROR: variant `enums::external_crate::TheirStruct::Unit` has no field named `0` + //~| ERROR: `?` operator has incompatible types + //~| ERROR: `?` operator has incompatible types + enum MyStruct { + // Reason: Should be unit variant + Unit(i32), + // Reason: Should be `u32` + Tuple(bool), + //~^ ERROR: mismatched types + // Reason: Should be `usize` + Struct { value: String }, + //~^ ERROR: mismatched types + } +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_pass.rs new file mode 100644 index 00000000000000..7f2fdb73dd6efc --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/invalid_definition_pass.rs @@ -0,0 +1,48 @@ +//@check-pass + +mod structs { + use bevy_reflect::reflect_remote; + + mod external_crate { + pub struct TheirStruct { + pub value: u32, + } + } + + #[reflect_remote(external_crate::TheirStruct)] + struct MyStruct { + pub value: u32, + } +} + +mod tuple_structs { + use bevy_reflect::reflect_remote; + + mod external_crate { + pub struct TheirStruct(pub u32); + } + + #[reflect_remote(external_crate::TheirStruct)] + struct MyStruct(pub u32); +} + +mod enums { + use bevy_reflect::reflect_remote; + + mod external_crate { + pub enum TheirStruct { + Unit, + Tuple(u32), + Struct { value: usize }, + } + } + + #[reflect_remote(external_crate::TheirStruct)] + enum MyStruct { + Unit, + Tuple(u32), + Struct { value: usize }, + } +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/macro_order_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/macro_order_fail.rs new file mode 100644 index 00000000000000..2114482816b29a --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/macro_order_fail.rs @@ -0,0 +1,19 @@ +use bevy_reflect::{reflect_remote, std_traits::ReflectDefault}; + +mod external_crate { + #[derive(Debug, Default)] + pub struct TheirType { + pub value: String, + } +} + +#[derive(Debug, Default)] +#[reflect_remote(external_crate::TheirType)] +#[reflect(Debug, Default)] +struct MyType { + pub value: String, + //~^ ERROR: no field `value` on type `&MyType` + //~| ERROR: struct `MyType` has no field named `value` +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/macro_order_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/macro_order_pass.rs new file mode 100644 index 00000000000000..58fcb56aaf8d32 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/macro_order_pass.rs @@ -0,0 +1,18 @@ +//@check-pass +use bevy_reflect::{reflect_remote, std_traits::ReflectDefault}; + +mod external_crate { + #[derive(Debug, Default)] + pub struct TheirType { + pub value: String, + } +} + +#[reflect_remote(external_crate::TheirType)] +#[derive(Debug, Default)] +#[reflect(Debug, Default)] +struct MyType { + pub value: String, +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs new file mode 100644 index 00000000000000..c9f85c000187d8 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_fail.rs @@ -0,0 +1,75 @@ +mod external_crate { + pub struct TheirOuter { + pub inner: TheirInner, + } + pub struct TheirInner(pub T); +} + +mod missing_attribute { + use bevy_reflect::{FromReflect, GetTypeRegistration, reflect_remote}; + + #[reflect_remote(super::external_crate::TheirOuter)] + struct MyOuter { + // Reason: Missing `#[reflect(remote = ...)]` attribute + pub inner: super::external_crate::TheirInner, + } + + #[reflect_remote(super::external_crate::TheirInner)] + struct MyInner(pub T); +} + +mod incorrect_inner_type { + use bevy_reflect::{FromReflect, GetTypeRegistration, reflect_remote}; + + #[reflect_remote(super::external_crate::TheirOuter)] + //~^ ERROR: `TheirInner` does not implement `PartialReflect` so cannot be introspected + //~| ERROR: `TheirInner` does not implement `PartialReflect` so cannot be introspected + //~| ERROR: `TheirInner` does not implement `PartialReflect` so cannot be introspected + //~| ERROR: `TheirInner` can not be used as a dynamic type path + //~| ERROR: `?` operator has incompatible types + struct MyOuter { + // Reason: Should not use `MyInner` directly + pub inner: MyInner, + //~^ ERROR: mismatched types + } + + #[reflect_remote(super::external_crate::TheirInner)] + struct MyInner(pub T); +} + +mod mismatched_remote_type { + use bevy_reflect::{FromReflect, GetTypeRegistration, reflect_remote}; + + #[reflect_remote(super::external_crate::TheirOuter)] + //~^ ERROR: mismatched types + //~| ERROR: mismatched types + struct MyOuter { + // Reason: Should be `MyInner` + #[reflect(remote = MyOuter)] + //~^ ERROR: mismatched types + pub inner: super::external_crate::TheirInner, + } + + #[reflect_remote(super::external_crate::TheirInner)] + struct MyInner(pub T); +} + +mod mismatched_remote_generic { + use bevy_reflect::{FromReflect, GetTypeRegistration, reflect_remote}; + + #[reflect_remote(super::external_crate::TheirOuter)] + //~^ ERROR: `?` operator has incompatible types + //~| ERROR: mismatched types + //~| ERROR: mismatched types + struct MyOuter { + // Reason: `TheirOuter::inner` is not defined as `TheirInner` + #[reflect(remote = MyInner)] + pub inner: super::external_crate::TheirInner, + //~^ ERROR: mismatched types + } + + #[reflect_remote(super::external_crate::TheirInner)] + struct MyInner(pub T); +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_pass.rs new file mode 100644 index 00000000000000..cd79ede57ab5db --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/nested_pass.rs @@ -0,0 +1,20 @@ +//@check-pass +use bevy_reflect::{FromReflect, GetTypeRegistration, reflect_remote, Reflect, Typed}; + +mod external_crate { + pub struct TheirOuter { + pub inner: TheirInner, + } + pub struct TheirInner(pub T); +} + +#[reflect_remote(external_crate::TheirOuter)] +struct MyOuter { + #[reflect(remote = MyInner)] + pub inner: external_crate::TheirInner, +} + +#[reflect_remote(external_crate::TheirInner)] +struct MyInner(pub T); + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs new file mode 100644 index 00000000000000..1983442ab7d3a5 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_fail.rs @@ -0,0 +1,99 @@ +//@no-rustfix + +mod structs { + use bevy_reflect::{reflect_remote, Reflect}; + + mod external_crate { + pub struct TheirFoo { + pub value: u32, + } + pub struct TheirBar { + pub value: i32, + } + } + + #[reflect_remote(external_crate::TheirFoo)] + struct MyFoo { + pub value: u32, + } + #[reflect_remote(external_crate::TheirBar)] + struct MyBar { + pub value: i32, + } + + #[derive(Reflect)] + //~^ ERROR: mismatched types + //~| ERROR: mismatched types + struct MyStruct { + // Reason: Should use `MyFoo` + #[reflect(remote = MyBar)] + //~^ ERROR: mismatched types + foo: external_crate::TheirFoo, + } +} + +mod tuple_structs { + use bevy_reflect::{reflect_remote, Reflect}; + + mod external_crate { + pub struct TheirFoo(pub u32); + + pub struct TheirBar(pub i32); + } + + #[reflect_remote(external_crate::TheirFoo)] + struct MyFoo(pub u32); + + #[reflect_remote(external_crate::TheirBar)] + struct MyBar(pub i32); + + #[derive(Reflect)] + //~^ ERROR: mismatched types + //~| ERROR: mismatched types + struct MyStruct( + // Reason: Should use `MyFoo` + #[reflect(remote = MyBar)] external_crate::TheirFoo, + //~^ ERROR: mismatched types + ); +} + +mod enums { + use bevy_reflect::{reflect_remote, Reflect}; + + mod external_crate { + pub enum TheirFoo { + Value(u32), + } + + pub enum TheirBar { + Value(i32), + } + } + + #[reflect_remote(external_crate::TheirFoo)] + enum MyFoo { + Value(u32), + } + + #[reflect_remote(external_crate::TheirBar)] + //~^ ERROR: `?` operator has incompatible types + enum MyBar { + // Reason: Should use `i32` + Value(u32), + //~^ ERROR: mismatched types + } + + #[derive(Reflect)] + //~^ ERROR: mismatched types + //~| ERROR: mismatched types + //~| ERROR: mismatched types + enum MyStruct { + Value( + // Reason: Should use `MyFoo` + #[reflect(remote = MyBar)] external_crate::TheirFoo, + //~^ ERROR: mismatched types + ), + } +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_pass.rs b/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_pass.rs new file mode 100644 index 00000000000000..78f9d52c315262 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/reflect_remote/type_mismatch_pass.rs @@ -0,0 +1,79 @@ +//@check-pass + +mod structs { + use bevy_reflect::{reflect_remote, Reflect}; + + mod external_crate { + pub struct TheirFoo { + pub value: u32, + } + pub struct TheirBar { + pub value: i32, + } + } + + #[reflect_remote(external_crate::TheirFoo)] + struct MyFoo { + pub value: u32, + } + #[reflect_remote(external_crate::TheirBar)] + struct MyBar { + pub value: i32, + } + + #[derive(Reflect)] + struct MyStruct { + #[reflect(remote = MyFoo)] + foo: external_crate::TheirFoo, + } +} + +mod tuple_structs { + use bevy_reflect::{reflect_remote, Reflect}; + + mod external_crate { + pub struct TheirFoo(pub u32); + + pub struct TheirBar(pub i32); + } + + #[reflect_remote(external_crate::TheirFoo)] + struct MyFoo(pub u32); + + #[reflect_remote(external_crate::TheirBar)] + struct MyBar(pub i32); + + #[derive(Reflect)] + struct MyStruct(#[reflect(remote = MyFoo)] external_crate::TheirFoo); +} + +mod enums { + use bevy_reflect::{reflect_remote, Reflect}; + + mod external_crate { + pub enum TheirFoo { + Value(u32), + } + + pub enum TheirBar { + Value(i32), + } + } + + #[reflect_remote(external_crate::TheirFoo)] + enum MyFoo { + Value(u32), + } + + #[reflect_remote(external_crate::TheirBar)] + enum MyBar { + Value(i32), + } + + #[derive(Reflect)] + enum MyStruct { + Value(#[reflect(remote = MyFoo)] external_crate::TheirFoo), + } +} + +fn main() {} diff --git a/crates/bevy_reflect/compile_fail/tests/remote.rs b/crates/bevy_reflect/compile_fail/tests/remote.rs new file mode 100644 index 00000000000000..3c843853a1aa70 --- /dev/null +++ b/crates/bevy_reflect/compile_fail/tests/remote.rs @@ -0,0 +1,3 @@ +fn main() -> compile_fail_utils::ui_test::Result<()> { + compile_fail_utils::test("reflect_remote", "tests/reflect_remote") +} diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index 76e897a9efc746..0df5d9af2383c2 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -15,6 +15,8 @@ proc-macro = true default = [] # When enabled, allows documentation comments to be processed by the reflection macros documentation = [] +# Enables macro logic related to function reflection +functions = [] [dependencies] bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } @@ -28,5 +30,5 @@ uuid = { version = "1.1", features = ["v4"] } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_reflect/derive/src/container_attributes.rs b/crates/bevy_reflect/derive/src/container_attributes.rs index 73637c252b39f5..b5f0f906b3bcc8 100644 --- a/crates/bevy_reflect/derive/src/container_attributes.rs +++ b/crates/bevy_reflect/derive/src/container_attributes.rs @@ -444,7 +444,7 @@ impl ContainerAttributes { &self.type_path_attrs } - /// Returns the implementation of `Reflect::reflect_hash` as a `TokenStream`. + /// Returns the implementation of `PartialReflect::reflect_hash` as a `TokenStream`. /// /// If `Hash` was not registered, returns `None`. pub fn get_hash_impl(&self, bevy_reflect_path: &Path) -> Option { @@ -467,7 +467,7 @@ impl ContainerAttributes { } } - /// Returns the implementation of `Reflect::reflect_partial_eq` as a `TokenStream`. + /// Returns the implementation of `PartialReflect::reflect_partial_eq` as a `TokenStream`. /// /// If `PartialEq` was not registered, returns `None`. pub fn get_partial_eq_impl( @@ -476,9 +476,9 @@ impl ContainerAttributes { ) -> Option { match &self.partial_eq { &TraitImpl::Implemented(span) => Some(quote_spanned! {span=> - fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - let value = ::as_any(value); - if let #FQOption::Some(value) = ::downcast_ref::(value) { + fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { + let value = ::try_downcast_ref::(value); + if let #FQOption::Some(value) = value { #FQOption::Some(::core::cmp::PartialEq::eq(self, value)) } else { #FQOption::Some(false) @@ -486,7 +486,7 @@ impl ContainerAttributes { } }), &TraitImpl::Custom(ref impl_fn, span) => Some(quote_spanned! {span=> - fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> #FQOption { + fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { #FQOption::Some(#impl_fn(self, value)) } }), @@ -494,7 +494,7 @@ impl ContainerAttributes { } } - /// Returns the implementation of `Reflect::debug` as a `TokenStream`. + /// Returns the implementation of `PartialReflect::debug` as a `TokenStream`. /// /// If `Debug` was not registered, returns `None`. pub fn get_debug_impl(&self) -> Option { diff --git a/crates/bevy_reflect/derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs index 8825febfffb0af..edb11f3ef32539 100644 --- a/crates/bevy_reflect/derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -8,6 +8,7 @@ use crate::utility::{StringExpr, WhereClauseOptions}; use quote::{quote, ToTokens}; use syn::token::Comma; +use crate::remote::RemoteType; use crate::serialization::SerializationDataDef; use crate::{ utility, REFLECT_ATTRIBUTE_NAME, REFLECT_VALUE_ATTRIBUTE_NAME, TYPE_NAME_ATTRIBUTE_NAME, @@ -46,6 +47,8 @@ pub(crate) struct ReflectMeta<'a> { attrs: ContainerAttributes, /// The path to this type. type_path: ReflectTypePath<'a>, + /// The optional remote type to use instead of the actual type. + remote_ty: Option>, /// A cached instance of the path to the `bevy_reflect` crate. bevy_reflect_path: Path, /// The documentation for this type, if any @@ -146,8 +149,12 @@ enum ReflectMode { /// How the macro was invoked. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) enum ReflectImplSource { + /// Using `impl_reflect!`. ImplRemoteType, + /// Using `#[derive(...)]`. DeriveLocalType, + /// Using `#[reflect_remote]`. + RemoteReflect, } /// Which trait the macro explicitly implements. @@ -173,7 +180,9 @@ impl fmt::Display for ReflectProvenance { (S::DeriveLocalType, T::Reflect) => "`#[derive(Reflect)]`", (S::DeriveLocalType, T::FromReflect) => "`#[derive(FromReflect)]`", (S::DeriveLocalType, T::TypePath) => "`#[derive(TypePath)]`", - (S::ImplRemoteType, T::FromReflect | T::TypePath) => unreachable!(), + (S::RemoteReflect, T::Reflect) => "`#[reflect_remote]`", + (S::RemoteReflect, T::FromReflect | T::TypePath) + | (S::ImplRemoteType, T::FromReflect | T::TypePath) => unreachable!(), }; f.write_str(str) } @@ -343,13 +352,52 @@ impl<'a> ReflectDerive<'a> { }; } - pub fn meta(&self) -> &ReflectMeta<'a> { + /// Set the remote type for this derived type. + /// + /// # Panics + /// + /// Panics when called on [`ReflectDerive::Value`]. + pub fn set_remote(&mut self, remote_ty: Option>) { + match self { + Self::Struct(data) | Self::TupleStruct(data) | Self::UnitStruct(data) => { + data.meta.remote_ty = remote_ty; + } + Self::Enum(data) => { + data.meta.remote_ty = remote_ty; + } + Self::Value(meta) => { + meta.remote_ty = remote_ty; + } + } + } + + /// Get the remote type path, if any. + pub fn remote_ty(&self) -> Option { + match self { + Self::Struct(data) | Self::TupleStruct(data) | Self::UnitStruct(data) => { + data.meta.remote_ty() + } + Self::Enum(data) => data.meta.remote_ty(), + Self::Value(meta) => meta.remote_ty(), + } + } + + /// Get the [`ReflectMeta`] for this derived type. + pub fn meta(&self) -> &ReflectMeta { match self { - ReflectDerive::Struct(data) - | ReflectDerive::TupleStruct(data) - | ReflectDerive::UnitStruct(data) => data.meta(), - ReflectDerive::Enum(data) => data.meta(), - ReflectDerive::Value(meta) => meta, + Self::Struct(data) | Self::TupleStruct(data) | Self::UnitStruct(data) => data.meta(), + Self::Enum(data) => data.meta(), + Self::Value(meta) => meta, + } + } + + pub fn where_clause_options(&self) -> WhereClauseOptions { + match self { + Self::Struct(data) | Self::TupleStruct(data) | Self::UnitStruct(data) => { + data.where_clause_options() + } + Self::Enum(data) => data.where_clause_options(), + Self::Value(meta) => WhereClauseOptions::new(meta), } } @@ -424,6 +472,7 @@ impl<'a> ReflectMeta<'a> { Self { attrs, type_path, + remote_ty: None, bevy_reflect_path: utility::get_bevy_reflect_path(), #[cfg(feature = "documentation")] docs: Default::default(), @@ -457,6 +506,16 @@ impl<'a> ReflectMeta<'a> { &self.type_path } + /// Get the remote type path, if any. + pub fn remote_ty(&self) -> Option { + self.remote_ty + } + + /// Whether this reflected type represents a remote type or not. + pub fn is_remote_wrapper(&self) -> bool { + self.remote_ty.is_some() + } + /// The cached `bevy_reflect` path. pub fn bevy_reflect_path(&self) -> &Path { &self.bevy_reflect_path @@ -500,7 +559,7 @@ impl<'a> StructField<'a> { } }; - let ty = &self.data.ty; + let ty = self.reflected_type(); let custom_attributes = self.attrs.custom_attributes.to_tokens(bevy_reflect_path); #[allow(unused_mut)] // Needs mutability for the feature gate @@ -518,6 +577,19 @@ impl<'a> StructField<'a> { info } + + /// Returns the reflected type of this field. + /// + /// Normally this is just the field's defined type. + /// However, this can be adjusted to use a different type, like for representing remote types. + /// In those cases, the returned value is the remote wrapper type. + pub fn reflected_type(&self) -> &Type { + self.attrs.remote.as_ref().unwrap_or(&self.data.ty) + } + + pub fn attrs(&self) -> &FieldAttributes { + &self.attrs + } } impl<'a> ReflectStruct<'a> { @@ -549,7 +621,7 @@ impl<'a> ReflectStruct<'a> { /// Get a collection of types which are exposed to the reflection API pub fn active_types(&self) -> Vec { self.active_fields() - .map(|field| field.data.ty.clone()) + .map(|field| field.reflected_type().clone()) .collect() } @@ -631,8 +703,18 @@ impl<'a> ReflectEnum<'a> { } /// Returns the given ident as a qualified unit variant of this enum. + /// + /// This takes into account the remote type, if any. pub fn get_unit(&self, variant: &Ident) -> proc_macro2::TokenStream { - let name = self.meta.type_path(); + let name = self + .meta + .remote_ty + .map(|path| match path.as_expr_path() { + Ok(path) => path.to_token_stream(), + Err(err) => err.into_compile_error(), + }) + .unwrap_or_else(|| self.meta.type_path().to_token_stream()); + quote! { #name::#variant } @@ -646,7 +728,7 @@ impl<'a> ReflectEnum<'a> { /// Get a collection of types which are exposed to the reflection API pub fn active_types(&self) -> Vec { self.active_fields() - .map(|field| field.data.ty.clone()) + .map(|field| field.reflected_type().clone()) .collect() } @@ -670,7 +752,7 @@ impl<'a> ReflectEnum<'a> { self.meta(), where_clause_options, None, - Some(self.active_fields().map(|field| &field.data.ty)), + Some(self.active_fields().map(StructField::reflected_type)), ) } @@ -1121,6 +1203,28 @@ impl<'a> ReflectTypePath<'a> { pub fn type_ident(&self) -> Option { self.get_ident().map(StringExpr::from) } + + /// Returns the true type regardless of whether a custom path is specified. + /// + /// To get the custom path if there is one, use [`Self::get_path`]. + /// + /// For example, the type `Foo` would return `Foo`. + pub fn true_type(&self) -> proc_macro2::TokenStream { + match self { + Self::Primitive(ident) => quote!(#ident), + Self::Internal { + ident, generics, .. + } => { + let (_, ty_generics, _) = generics.split_for_impl(); + quote!(#ident #ty_generics) + } + Self::External { path, generics, .. } => { + let (_, ty_generics, _) = generics.split_for_impl(); + quote!(#path #ty_generics) + } + Self::Anonymous { qualified_type, .. } => qualified_type.to_token_stream(), + } + } } impl<'a> ToTokens for ReflectTypePath<'a> { diff --git a/crates/bevy_reflect/derive/src/enum_utility.rs b/crates/bevy_reflect/derive/src/enum_utility.rs index 9f7a12aa70df7e..a279a82237500a 100644 --- a/crates/bevy_reflect/derive/src/enum_utility.rs +++ b/crates/bevy_reflect/derive/src/enum_utility.rs @@ -79,12 +79,14 @@ pub(crate) trait VariantBuilder: Sized { /// * `this`: The identifier of the enum /// * `field`: The field to access fn on_active_field(&self, this: &Ident, field: VariantField) -> TokenStream { + let bevy_reflect_path = self.reflect_enum().meta().bevy_reflect_path(); let field_accessor = self.access_field(this, field); let alias = field.alias; + let field_ty = field.field.reflected_type(); let field_constructor = self.construct_field(field); - match &field.field.attrs.default { + let construction = match &field.field.attrs.default { DefaultBehavior::Func(path) => quote! { if let #FQOption::Some(#alias) = #field_accessor { #field_constructor @@ -109,6 +111,14 @@ pub(crate) trait VariantBuilder: Sized { #field_constructor }} } + }; + + if field.field.attrs().remote.is_some() { + quote! { + <#field_ty as #bevy_reflect_path::ReflectRemote>::into_remote(#construction) + } + } else { + construction } } @@ -200,7 +210,7 @@ impl<'a> VariantBuilder for FromReflectVariantBuilder<'a> { fn construct_field(&self, field: VariantField) -> TokenStream { let bevy_reflect_path = self.reflect_enum.meta().bevy_reflect_path(); - let field_ty = &field.field.data.ty; + let field_ty = field.field.reflected_type(); let alias = field.alias; quote! { @@ -209,7 +219,7 @@ impl<'a> VariantBuilder for FromReflectVariantBuilder<'a> { } } -/// Generates the enum variant output data needed to build the `Reflect::try_apply` implementation. +/// Generates the enum variant output data needed to build the `PartialReflect::try_apply` implementation. pub(crate) struct TryApplyVariantBuilder<'a> { reflect_enum: &'a ReflectEnum<'a>, } @@ -251,7 +261,7 @@ impl<'a> VariantBuilder for TryApplyVariantBuilder<'a> { fn construct_field(&self, field: VariantField) -> TokenStream { let bevy_reflect_path = self.reflect_enum.meta().bevy_reflect_path(); let alias = field.alias; - let field_ty = &field.field.data.ty; + let field_ty = field.field.reflected_type(); quote! { <#field_ty as #bevy_reflect_path::FromReflect>::from_reflect(#alias) diff --git a/crates/bevy_reflect/derive/src/field_attributes.rs b/crates/bevy_reflect/derive/src/field_attributes.rs index d66daf8382b4d6..5228f80a67cdf6 100644 --- a/crates/bevy_reflect/derive/src/field_attributes.rs +++ b/crates/bevy_reflect/derive/src/field_attributes.rs @@ -7,13 +7,15 @@ use crate::custom_attributes::CustomAttributes; use crate::utility::terminated_parser; use crate::REFLECT_ATTRIBUTE_NAME; +use quote::ToTokens; use syn::parse::ParseStream; -use syn::{Attribute, LitStr, Meta, Token}; +use syn::{Attribute, LitStr, Meta, Token, Type}; mod kw { syn::custom_keyword!(ignore); syn::custom_keyword!(skip_serializing); syn::custom_keyword!(default); + syn::custom_keyword!(remote); } pub(crate) const IGNORE_SERIALIZATION_ATTR: &str = "skip_serializing"; @@ -76,6 +78,8 @@ pub(crate) struct FieldAttributes { pub default: DefaultBehavior, /// Custom attributes created via `#[reflect(@...)]`. pub custom_attributes: CustomAttributes, + /// For defining the remote wrapper type that should be used in place of the field for reflection logic. + pub remote: Option, } impl FieldAttributes { @@ -119,6 +123,8 @@ impl FieldAttributes { self.parse_skip_serializing(input) } else if lookahead.peek(kw::default) { self.parse_default(input) + } else if lookahead.peek(kw::remote) { + self.parse_remote(input) } else { Err(lookahead.error()) } @@ -190,4 +196,41 @@ impl FieldAttributes { fn parse_custom_attribute(&mut self, input: ParseStream) -> syn::Result<()> { self.custom_attributes.parse_custom_attribute(input) } + + /// Parse `remote` attribute. + /// + /// Examples: + /// - `#[reflect(remote = path::to::RemoteType)]` + fn parse_remote(&mut self, input: ParseStream) -> syn::Result<()> { + if let Some(remote) = self.remote.as_ref() { + return Err(input.error(format!( + "remote type already specified as {}", + remote.to_token_stream() + ))); + } + + input.parse::()?; + input.parse::()?; + + self.remote = Some(input.parse()?); + + Ok(()) + } + + /// Returns `Some(true)` if the field has a generic remote type. + /// + /// If the remote type is not generic, returns `Some(false)`. + /// + /// If the field does not have a remote type, returns `None`. + pub fn is_remote_generic(&self) -> Option { + if let Type::Path(type_path) = self.remote.as_ref()? { + type_path + .path + .segments + .last() + .map(|segment| !segment.arguments.is_empty()) + } else { + Some(false) + } + } } diff --git a/crates/bevy_reflect/derive/src/from_reflect.rs b/crates/bevy_reflect/derive/src/from_reflect.rs index abc7fb8929ce9d..9ed0d1fcaa4354 100644 --- a/crates/bevy_reflect/derive/src/from_reflect.rs +++ b/crates/bevy_reflect/derive/src/from_reflect.rs @@ -4,7 +4,7 @@ use crate::enum_utility::{EnumVariantOutputData, FromReflectVariantBuilder, Vari use crate::field_attributes::DefaultBehavior; use crate::utility::{ident_or_index, WhereClauseOptions}; use crate::{ReflectMeta, ReflectStruct}; -use bevy_macro_utils::fq_std::{FQAny, FQClone, FQDefault, FQOption}; +use bevy_macro_utils::fq_std::{FQClone, FQDefault, FQOption}; use proc_macro2::Span; use quote::{quote, ToTokens}; use syn::{Field, Ident, Lit, LitInt, LitStr, Member}; @@ -26,8 +26,12 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { let where_from_reflect_clause = WhereClauseOptions::new(meta).extend_where_clause(where_clause); quote! { impl #impl_generics #bevy_reflect_path::FromReflect for #type_path #ty_generics #where_from_reflect_clause { - fn from_reflect(reflect: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - #FQOption::Some(#FQClone::clone(::downcast_ref::<#type_path #ty_generics>(::as_any(reflect))?)) + fn from_reflect(reflect: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { + #FQOption::Some( + #FQClone::clone( + ::try_downcast_ref::<#type_path #ty_generics>(reflect)? + ) + ) } } } @@ -48,6 +52,16 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream .. } = FromReflectVariantBuilder::new(reflect_enum).build(&ref_value); + let match_branches = if reflect_enum.meta().is_remote_wrapper() { + quote! { + #(#variant_names => #fqoption::Some(Self(#variant_constructors)),)* + } + } else { + quote! { + #(#variant_names => #fqoption::Some(#variant_constructors),)* + } + }; + let (impl_generics, ty_generics, where_clause) = enum_path.generics().split_for_impl(); // Add FromReflect bound for each active field @@ -57,10 +71,12 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream quote! { impl #impl_generics #bevy_reflect_path::FromReflect for #enum_path #ty_generics #where_from_reflect_clause { - fn from_reflect(#ref_value: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - if let #bevy_reflect_path::ReflectRef::Enum(#ref_value) = #bevy_reflect_path::Reflect::reflect_ref(#ref_value) { + fn from_reflect(#ref_value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { + if let #bevy_reflect_path::ReflectRef::Enum(#ref_value) = + #bevy_reflect_path::PartialReflect::reflect_ref(#ref_value) + { match #bevy_reflect_path::Enum::variant_name(#ref_value) { - #(#variant_names => #fqoption::Some(#variant_constructors),)* + #match_branches name => panic!("variant with name `{}` does not exist on enum `{}`", name, ::type_path()), } } else { @@ -88,6 +104,7 @@ fn impl_struct_internal( let fqoption = FQOption.into_token_stream(); let struct_path = reflect_struct.meta().type_path(); + let remote_ty = reflect_struct.meta().remote_ty(); let bevy_reflect_path = reflect_struct.meta().bevy_reflect_path(); let ref_struct = Ident::new("__ref_struct", Span::call_site()); @@ -101,28 +118,48 @@ fn impl_struct_internal( get_active_fields(reflect_struct, &ref_struct, &ref_struct_type, is_tuple); let is_defaultable = reflect_struct.meta().attrs().contains(REFLECT_DEFAULT); + + // The constructed "Self" ident + let __this = Ident::new("__this", Span::call_site()); + + // The reflected type: either `Self` or a remote type + let (reflect_ty, constructor, retval) = if let Some(remote_ty) = remote_ty { + let constructor = match remote_ty.as_expr_path() { + Ok(path) => path, + Err(err) => return err.into_compile_error(), + }; + let remote_ty = remote_ty.type_path(); + + ( + quote!(#remote_ty), + quote!(#constructor), + quote!(Self(#__this)), + ) + } else { + (quote!(Self), quote!(Self), quote!(#__this)) + }; + let constructor = if is_defaultable { - quote!( - let mut __this: Self = #FQDefault::default(); + quote! { + let mut #__this = <#reflect_ty as #FQDefault>::default(); #( if let #fqoption::Some(__field) = #active_values() { // Iff field exists -> use its value - __this.#active_members = __field; + #__this.#active_members = __field; } )* - #FQOption::Some(__this) - ) + #FQOption::Some(#retval) + } } else { let MemberValuePair(ignored_members, ignored_values) = get_ignored_fields(reflect_struct); - quote!( - #FQOption::Some( - Self { - #(#active_members: #active_values()?,)* - #(#ignored_members: #ignored_values,)* - } - ) - ) + quote! { + let #__this = #constructor { + #(#active_members: #active_values()?,)* + #(#ignored_members: #ignored_values,)* + }; + #FQOption::Some(#retval) + } }; let (impl_generics, ty_generics, where_clause) = reflect_struct @@ -138,8 +175,10 @@ fn impl_struct_internal( quote! { impl #impl_generics #bevy_reflect_path::FromReflect for #struct_path #ty_generics #where_from_reflect_clause { - fn from_reflect(reflect: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - if let #bevy_reflect_path::ReflectRef::#ref_struct_type(#ref_struct) = #bevy_reflect_path::Reflect::reflect_ref(reflect) { + fn from_reflect(reflect: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { + if let #bevy_reflect_path::ReflectRef::#ref_struct_type(#ref_struct) + = #bevy_reflect_path::PartialReflect::reflect_ref(reflect) + { #constructor } else { #FQOption::None @@ -193,34 +232,76 @@ fn get_active_fields( field.reflection_index.expect("field should be active"), is_tuple, ); - let ty = field.data.ty.clone(); + let ty = field.reflected_type().clone(); + let real_ty = &field.data.ty; let get_field = quote! { #bevy_reflect_path::#struct_type::field(#dyn_struct_name, #accessor) }; + let into_remote = |value: proc_macro2::TokenStream| { + if field.attrs.is_remote_generic().unwrap_or_default() { + quote! { + #FQOption::Some( + // SAFETY: The remote type should always be a `#[repr(transparent)]` for the actual field type + unsafe { + ::core::mem::transmute_copy::<#ty, #real_ty>( + &::core::mem::ManuallyDrop::new(#value?) + ) + } + ) + } + } else if field.attrs().remote.is_some() { + quote! { + #FQOption::Some( + // SAFETY: The remote type should always be a `#[repr(transparent)]` for the actual field type + unsafe { + ::core::mem::transmute::<#ty, #real_ty>(#value?) + } + ) + } + } else { + value + } + }; + let value = match &field.attrs.default { - DefaultBehavior::Func(path) => quote! { - (|| - if let #FQOption::Some(field) = #get_field { - <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) - } else { - #FQOption::Some(#path()) - } - ) - }, - DefaultBehavior::Default => quote! { - (|| - if let #FQOption::Some(field) = #get_field { - <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) - } else { - #FQOption::Some(#FQDefault::default()) - } - ) - }, - DefaultBehavior::Required => quote! { - (|| <#ty as #bevy_reflect_path::FromReflect>::from_reflect(#get_field?)) - }, + DefaultBehavior::Func(path) => { + let value = into_remote(quote! { + <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) + }); + quote! { + (|| + if let #FQOption::Some(field) = #get_field { + #value + } else { + #FQOption::Some(#path()) + } + ) + } + } + DefaultBehavior::Default => { + let value = into_remote(quote! { + <#ty as #bevy_reflect_path::FromReflect>::from_reflect(field) + }); + quote! { + (|| + if let #FQOption::Some(field) = #get_field { + #value + } else { + #FQOption::Some(#FQDefault::default()) + } + ) + } + } + DefaultBehavior::Required => { + let value = into_remote(quote! { + <#ty as #bevy_reflect_path::FromReflect>::from_reflect(#get_field?) + }); + quote! { + (|| #value) + } + } }; (member, value) diff --git a/crates/bevy_reflect/derive/src/impls/assertions.rs b/crates/bevy_reflect/derive/src/impls/assertions.rs new file mode 100644 index 00000000000000..06d835e6cf9d40 --- /dev/null +++ b/crates/bevy_reflect/derive/src/impls/assertions.rs @@ -0,0 +1,14 @@ +use crate::derive_data::ReflectDerive; +use crate::remote::generate_remote_assertions; +use quote::quote; + +/// Generates an anonymous block containing compile-time assertions. +pub(crate) fn impl_assertions(derive_data: &ReflectDerive) -> proc_macro2::TokenStream { + let mut output = quote!(); + + if let Some(assertions) = generate_remote_assertions(derive_data) { + output.extend(assertions); + } + + output +} diff --git a/crates/bevy_reflect/derive/src/impls/common.rs b/crates/bevy_reflect/derive/src/impls/common.rs new file mode 100644 index 00000000000000..f01fd96e044ccf --- /dev/null +++ b/crates/bevy_reflect/derive/src/impls/common.rs @@ -0,0 +1,158 @@ +use bevy_macro_utils::fq_std::{FQAny, FQBox, FQOption, FQResult}; + +use quote::quote; + +use crate::{derive_data::ReflectMeta, utility::WhereClauseOptions}; + +pub fn impl_full_reflect( + meta: &ReflectMeta, + where_clause_options: &WhereClauseOptions, +) -> proc_macro2::TokenStream { + let bevy_reflect_path = meta.bevy_reflect_path(); + let type_path = meta.type_path(); + + let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl(); + let where_reflect_clause = where_clause_options.extend_where_clause(where_clause); + + let any_impls = if meta.is_remote_wrapper() { + quote! { + #[inline] + fn into_any(self: #FQBox) -> #FQBox { + #FQBox::new(self.0) + } + + #[inline] + fn as_any(&self) -> &dyn #FQAny { + &self.0 + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn #FQAny { + &mut self.0 + } + } + } else { + quote! { + #[inline] + fn into_any(self: #FQBox) -> #FQBox { + self + } + + #[inline] + fn as_any(&self) -> &dyn #FQAny { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn #FQAny { + self + } + } + }; + + quote! { + impl #impl_generics #bevy_reflect_path::Reflect for #type_path #ty_generics #where_reflect_clause { + #any_impls + + #[inline] + fn into_reflect(self: #FQBox) -> #FQBox { + self + } + + #[inline] + fn as_reflect(&self) -> &dyn #bevy_reflect_path::Reflect { + self + } + + #[inline] + fn as_reflect_mut(&mut self) -> &mut dyn #bevy_reflect_path::Reflect { + self + } + + #[inline] + fn set( + &mut self, + value: #FQBox + ) -> #FQResult<(), #FQBox> { + *self = ::take(value)?; + #FQResult::Ok(()) + } + } + } +} + +pub fn common_partial_reflect_methods( + meta: &ReflectMeta, + default_partial_eq_delegate: impl FnOnce() -> Option, + default_hash_delegate: impl FnOnce() -> Option, +) -> proc_macro2::TokenStream { + let bevy_reflect_path = meta.bevy_reflect_path(); + + let debug_fn = meta.attrs().get_debug_impl(); + let partial_eq_fn = meta + .attrs() + .get_partial_eq_impl(bevy_reflect_path) + .or_else(move || { + let default_delegate = default_partial_eq_delegate(); + default_delegate.map(|func| { + quote! { + fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { + (#func)(self, value) + } + } + }) + }); + let hash_fn = meta + .attrs() + .get_hash_impl(bevy_reflect_path) + .or_else(move || { + let default_delegate = default_hash_delegate(); + default_delegate.map(|func| { + quote! { + fn reflect_hash(&self) -> #FQOption { + (#func)(self) + } + } + }) + }); + + quote! { + #[inline] + fn try_into_reflect( + self: #FQBox + ) -> #FQResult<#FQBox, #FQBox> { + #FQResult::Ok(self) + } + + #[inline] + fn try_as_reflect(&self) -> #FQOption<&dyn #bevy_reflect_path::Reflect> { + #FQOption::Some(self) + } + + #[inline] + fn try_as_reflect_mut(&mut self) -> #FQOption<&mut dyn #bevy_reflect_path::Reflect> { + #FQOption::Some(self) + } + + #[inline] + fn into_partial_reflect(self: #FQBox) -> #FQBox { + self + } + + #[inline] + fn as_partial_reflect(&self) -> &dyn #bevy_reflect_path::PartialReflect { + self + } + + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn #bevy_reflect_path::PartialReflect { + self + } + + #hash_fn + + #partial_eq_fn + + #debug_fn + } +} diff --git a/crates/bevy_reflect/derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs index 2d479ac2b94e8e..15d5c01ffa796f 100644 --- a/crates/bevy_reflect/derive/src/impls/enums.rs +++ b/crates/bevy_reflect/derive/src/impls/enums.rs @@ -1,14 +1,34 @@ use crate::derive_data::{EnumVariantFields, ReflectEnum, StructField}; use crate::enum_utility::{EnumVariantOutputData, TryApplyVariantBuilder, VariantBuilder}; -use crate::impls::{impl_function_traits, impl_type_path, impl_typed}; -use bevy_macro_utils::fq_std::{FQAny, FQBox, FQOption, FQResult}; +use crate::impls::{common_partial_reflect_methods, impl_full_reflect, impl_type_path, impl_typed}; +use bevy_macro_utils::fq_std::{FQBox, FQOption, FQResult}; use proc_macro2::{Ident, Span}; use quote::quote; -use syn::Fields; +use syn::{Fields, Path}; pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream { let bevy_reflect_path = reflect_enum.meta().bevy_reflect_path(); let enum_path = reflect_enum.meta().type_path(); + let is_remote = reflect_enum.meta().is_remote_wrapper(); + + // For `match self` expressions where self is a reference + let match_this = if is_remote { + quote!(&self.0) + } else { + quote!(self) + }; + // For `match self` expressions where self is a mutable reference + let match_this_mut = if is_remote { + quote!(&mut self.0) + } else { + quote!(self) + }; + // For `*self` assignments + let deref_this = if is_remote { + quote!(self.0) + } else { + quote!(*self) + }; let ref_name = Ident::new("__name_param", Span::call_site()); let ref_index = Ident::new("__index_param", Span::call_site()); @@ -18,7 +38,9 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream let EnumImpls { enum_field, + enum_field_mut, enum_field_at, + enum_field_at_mut, enum_index_of, enum_name_at, enum_field_len, @@ -33,30 +55,6 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream .. } = TryApplyVariantBuilder::new(reflect_enum).build(&ref_value); - let hash_fn = reflect_enum - .meta() - .attrs() - .get_hash_impl(bevy_reflect_path) - .unwrap_or_else(|| { - quote! { - fn reflect_hash(&self) -> #FQOption { - #bevy_reflect_path::enum_hash(self) - } - } - }); - let debug_fn = reflect_enum.meta().attrs().get_debug_impl(); - let partial_eq_fn = reflect_enum - .meta() - .attrs() - .get_partial_eq_impl(bevy_reflect_path) - .unwrap_or_else(|| { - quote! { - fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - #bevy_reflect_path::enum_partial_eq(self, value) - } - } - }); - let typed_impl = impl_typed( reflect_enum.meta(), &where_clause_options, @@ -64,8 +62,18 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream ); let type_path_impl = impl_type_path(reflect_enum.meta()); + let full_reflect_impl = impl_full_reflect(reflect_enum.meta(), &where_clause_options); + let common_methods = common_partial_reflect_methods( + reflect_enum.meta(), + || Some(quote!(#bevy_reflect_path::enum_partial_eq)), + || Some(quote!(#bevy_reflect_path::enum_hash)), + ); - let function_impls = impl_function_traits(reflect_enum.meta(), &where_clause_options); + #[cfg(not(feature = "functions"))] + let function_impls = None::; + #[cfg(feature = "functions")] + let function_impls = + crate::impls::impl_function_traits(reflect_enum.meta(), &where_clause_options); let get_type_registration_impl = reflect_enum.get_type_registration(&where_clause_options); @@ -81,46 +89,48 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #type_path_impl + #full_reflect_impl + #function_impls impl #impl_generics #bevy_reflect_path::Enum for #enum_path #ty_generics #where_reflect_clause { - fn field(&self, #ref_name: &str) -> #FQOption<&dyn #bevy_reflect_path::Reflect> { - match self { + fn field(&self, #ref_name: &str) -> #FQOption<&dyn #bevy_reflect_path::PartialReflect> { + match #match_this { #(#enum_field,)* _ => #FQOption::None, } } - fn field_at(&self, #ref_index: usize) -> #FQOption<&dyn #bevy_reflect_path::Reflect> { - match self { + fn field_at(&self, #ref_index: usize) -> #FQOption<&dyn #bevy_reflect_path::PartialReflect> { + match #match_this { #(#enum_field_at,)* _ => #FQOption::None, } } - fn field_mut(&mut self, #ref_name: &str) -> #FQOption<&mut dyn #bevy_reflect_path::Reflect> { - match self { - #(#enum_field,)* + fn field_mut(&mut self, #ref_name: &str) -> #FQOption<&mut dyn #bevy_reflect_path::PartialReflect> { + match #match_this_mut { + #(#enum_field_mut,)* _ => #FQOption::None, } } - fn field_at_mut(&mut self, #ref_index: usize) -> #FQOption<&mut dyn #bevy_reflect_path::Reflect> { - match self { - #(#enum_field_at,)* + fn field_at_mut(&mut self, #ref_index: usize) -> #FQOption<&mut dyn #bevy_reflect_path::PartialReflect> { + match #match_this_mut { + #(#enum_field_at_mut,)* _ => #FQOption::None, } } fn index_of(&self, #ref_name: &str) -> #FQOption { - match self { + match #match_this { #(#enum_index_of,)* _ => #FQOption::None, } } fn name_at(&self, #ref_index: usize) -> #FQOption<&str> { - match self { + match #match_this { #(#enum_name_at,)* _ => #FQOption::None, } @@ -132,7 +142,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #[inline] fn field_len(&self) -> usize { - match self { + match #match_this { #(#enum_field_len,)* _ => 0, } @@ -140,7 +150,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #[inline] fn variant_name(&self) -> &str { - match self { + match #match_this { #(#enum_variant_name,)* _ => unreachable!(), } @@ -148,7 +158,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #[inline] fn variant_index(&self) -> usize { - match self { + match #match_this { #(#enum_variant_index,)* _ => unreachable!(), } @@ -156,7 +166,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #[inline] fn variant_type(&self) -> #bevy_reflect_path::VariantType { - match self { + match #match_this { #(#enum_variant_type,)* _ => unreachable!(), } @@ -167,56 +177,24 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream } } - impl #impl_generics #bevy_reflect_path::Reflect for #enum_path #ty_generics #where_reflect_clause { + impl #impl_generics #bevy_reflect_path::PartialReflect for #enum_path #ty_generics #where_reflect_clause { #[inline] fn get_represented_type_info(&self) -> #FQOption<&'static #bevy_reflect_path::TypeInfo> { #FQOption::Some(::type_info()) } #[inline] - fn into_any(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_any(&self) -> &dyn #FQAny { - self - } - - #[inline] - fn as_any_mut(&mut self) -> &mut dyn #FQAny { - self - } - - #[inline] - fn into_reflect(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_reflect(&self) -> &dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn clone_value(&self) -> #FQBox { + fn clone_value(&self) -> #FQBox { #FQBox::new(#bevy_reflect_path::Enum::clone_dynamic(self)) } #[inline] - fn set(&mut self, #ref_value: #FQBox) -> #FQResult<(), #FQBox> { - *self = ::take(#ref_value)?; - #FQResult::Ok(()) - } - - #[inline] - fn try_apply(&mut self, #ref_value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { - if let #bevy_reflect_path::ReflectRef::Enum(#ref_value) = #bevy_reflect_path::Reflect::reflect_ref(#ref_value) { + fn try_apply( + &mut self, + #ref_value: &dyn #bevy_reflect_path::PartialReflect + ) -> #FQResult<(), #bevy_reflect_path::ApplyError> { + if let #bevy_reflect_path::ReflectRef::Enum(#ref_value) = + #bevy_reflect_path::PartialReflect::reflect_ref(#ref_value) { if #bevy_reflect_path::Enum::variant_name(self) == #bevy_reflect_path::Enum::variant_name(#ref_value) { // Same variant -> just update fields match #bevy_reflect_path::Enum::variant_type(#ref_value) { @@ -224,14 +202,14 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream for field in #bevy_reflect_path::Enum::iter_fields(#ref_value) { let name = field.name().unwrap(); if let #FQOption::Some(v) = #bevy_reflect_path::Enum::field_mut(self, name) { - #bevy_reflect_path::Reflect::try_apply(v, field.value())?; + #bevy_reflect_path::PartialReflect::try_apply(v, field.value())?; } } } #bevy_reflect_path::VariantType::Tuple => { for (index, field) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::Enum::iter_fields(#ref_value)) { if let #FQOption::Some(v) = #bevy_reflect_path::Enum::field_at_mut(self, index) { - #bevy_reflect_path::Reflect::try_apply(v, field.value())?; + #bevy_reflect_path::PartialReflect::try_apply(v, field.value())?; } } } @@ -241,7 +219,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream // New variant -> perform a switch match #bevy_reflect_path::Enum::variant_name(#ref_value) { #(#variant_names => { - *self = #variant_constructors + #deref_this = #variant_constructors })* name => { return #FQResult::Err( @@ -256,7 +234,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream } else { return #FQResult::Err( #bevy_reflect_path::ApplyError::MismatchedKinds { - from_kind: #bevy_reflect_path::Reflect::reflect_kind(#ref_value), + from_kind: #bevy_reflect_path::PartialReflect::reflect_kind(#ref_value), to_kind: #bevy_reflect_path::ReflectKind::Enum, } ); @@ -280,18 +258,16 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #bevy_reflect_path::ReflectOwned::Enum(self) } - #hash_fn - - #partial_eq_fn - - #debug_fn + #common_methods } } } struct EnumImpls { enum_field: Vec, + enum_field_mut: Vec, enum_field_at: Vec, + enum_field_at_mut: Vec, enum_index_of: Vec, enum_name_at: Vec, enum_field_len: Vec, @@ -304,7 +280,9 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden let bevy_reflect_path = reflect_enum.meta().bevy_reflect_path(); let mut enum_field = Vec::new(); + let mut enum_field_mut = Vec::new(); let mut enum_field_at = Vec::new(); + let mut enum_field_at_mut = Vec::new(); let mut enum_index_of = Vec::new(); let mut enum_name_at = Vec::new(); let mut enum_field_len = Vec::new(); @@ -352,6 +330,29 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden field_len } + /// Process the field value to account for remote types. + /// + /// If the field is a remote type, then the value will be transmuted accordingly. + fn process_field_value( + ident: &Ident, + field: &StructField, + is_mutable: bool, + bevy_reflect_path: &Path, + ) -> proc_macro2::TokenStream { + let method = if is_mutable { + quote!(as_wrapper_mut) + } else { + quote!(as_wrapper) + }; + + field + .attrs + .remote + .as_ref() + .map(|ty| quote!(<#ty as #bevy_reflect_path::ReflectRemote>::#method(#ident))) + .unwrap_or_else(|| quote!(#ident)) + } + match &variant.fields { EnumVariantFields::Unit => { let field_len = process_fields(&[], |_| {}); @@ -367,8 +368,16 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden .expect("reflection index should exist for active field"); let declare_field = syn::Index::from(field.declaration_index); + + let __value = Ident::new("__value", Span::call_site()); + let value_ref = process_field_value(&__value, field, false, bevy_reflect_path); + let value_mut = process_field_value(&__value, field, true, bevy_reflect_path); + enum_field_at.push(quote! { - #unit { #declare_field : value, .. } if #ref_index == #reflection_index => #FQOption::Some(value) + #unit { #declare_field : #__value, .. } if #ref_index == #reflection_index => #FQOption::Some(#value_ref) + }); + enum_field_at_mut.push(quote! { + #unit { #declare_field : #__value, .. } if #ref_index == #reflection_index => #FQOption::Some(#value_mut) }); }); @@ -384,11 +393,21 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden .reflection_index .expect("reflection index should exist for active field"); + let __value = Ident::new("__value", Span::call_site()); + let value_ref = process_field_value(&__value, field, false, bevy_reflect_path); + let value_mut = process_field_value(&__value, field, true, bevy_reflect_path); + enum_field.push(quote! { - #unit{ #field_ident, .. } if #ref_name == #field_name => #FQOption::Some(#field_ident) + #unit{ #field_ident: #__value, .. } if #ref_name == #field_name => #FQOption::Some(#value_ref) + }); + enum_field_mut.push(quote! { + #unit{ #field_ident: #__value, .. } if #ref_name == #field_name => #FQOption::Some(#value_mut) }); enum_field_at.push(quote! { - #unit{ #field_ident, .. } if #ref_index == #reflection_index => #FQOption::Some(#field_ident) + #unit{ #field_ident: #__value, .. } if #ref_index == #reflection_index => #FQOption::Some(#value_ref) + }); + enum_field_at_mut.push(quote! { + #unit{ #field_ident: #__value, .. } if #ref_index == #reflection_index => #FQOption::Some(#value_mut) }); enum_index_of.push(quote! { #unit{ .. } if #ref_name == #field_name => #FQOption::Some(#reflection_index) @@ -407,7 +426,9 @@ fn generate_impls(reflect_enum: &ReflectEnum, ref_index: &Ident, ref_name: &Iden EnumImpls { enum_field, + enum_field_mut, enum_field_at, + enum_field_at_mut, enum_index_of, enum_name_at, enum_field_len, diff --git a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs index a52f3e2fb43fdb..9757dd415c537d 100644 --- a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs +++ b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs @@ -15,32 +15,23 @@ pub(crate) fn impl_from_arg( quote! { impl #impl_generics #bevy_reflect::func::args::FromArg for #type_path #ty_generics #where_reflect_clause { - type Item<'from_arg> = #type_path #ty_generics; - fn from_arg<'from_arg>( - arg: #bevy_reflect::func::args::Arg<'from_arg>, - info: &#bevy_reflect::func::args::ArgInfo, - ) -> #FQResult, #bevy_reflect::func::args::ArgError> { - arg.take_owned(info) + type This<'from_arg> = #type_path #ty_generics; + fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult, #bevy_reflect::func::args::ArgError> { + arg.take_owned() } } impl #impl_generics #bevy_reflect::func::args::FromArg for &'static #type_path #ty_generics #where_reflect_clause { - type Item<'from_arg> = &'from_arg #type_path #ty_generics; - fn from_arg<'from_arg>( - arg: #bevy_reflect::func::args::Arg<'from_arg>, - info: &#bevy_reflect::func::args::ArgInfo, - ) -> #FQResult, #bevy_reflect::func::args::ArgError> { - arg.take_ref(info) + type This<'from_arg> = &'from_arg #type_path #ty_generics; + fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult, #bevy_reflect::func::args::ArgError> { + arg.take_ref() } } impl #impl_generics #bevy_reflect::func::args::FromArg for &'static mut #type_path #ty_generics #where_reflect_clause { - type Item<'from_arg> = &'from_arg mut #type_path #ty_generics; - fn from_arg<'from_arg>( - arg: #bevy_reflect::func::args::Arg<'from_arg>, - info: &#bevy_reflect::func::args::ArgInfo, - ) -> #FQResult, #bevy_reflect::func::args::ArgError> { - arg.take_mut(info) + type This<'from_arg> = &'from_arg mut #type_path #ty_generics; + fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult, #bevy_reflect::func::args::ArgError> { + arg.take_mut() } } } diff --git a/crates/bevy_reflect/derive/src/impls/func/into_return.rs b/crates/bevy_reflect/derive/src/impls/func/into_return.rs index 02c9fcf67ade18..db964b3cbbe124 100644 --- a/crates/bevy_reflect/derive/src/impls/func/into_return.rs +++ b/crates/bevy_reflect/derive/src/impls/func/into_return.rs @@ -14,19 +14,19 @@ pub(crate) fn impl_into_return( quote! { impl #impl_generics #bevy_reflect::func::IntoReturn for #type_path #ty_generics #where_reflect_clause { - fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> { + fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { #bevy_reflect::func::Return::Owned(Box::new(self)) } } - impl #impl_generics #bevy_reflect::func::IntoReturn for &'static #type_path #ty_generics #where_reflect_clause { - fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> { + impl #impl_generics #bevy_reflect::func::IntoReturn for &#type_path #ty_generics #where_reflect_clause { + fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { #bevy_reflect::func::Return::Ref(self) } } - impl #impl_generics #bevy_reflect::func::IntoReturn for &'static mut #type_path #ty_generics #where_reflect_clause { - fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> { + impl #impl_generics #bevy_reflect::func::IntoReturn for &mut #type_path #ty_generics #where_reflect_clause { + fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { #bevy_reflect::func::Return::Mut(self) } } diff --git a/crates/bevy_reflect/derive/src/impls/mod.rs b/crates/bevy_reflect/derive/src/impls/mod.rs index 276101bf8bbb5c..0905d43bd260ef 100644 --- a/crates/bevy_reflect/derive/src/impls/mod.rs +++ b/crates/bevy_reflect/derive/src/impls/mod.rs @@ -1,11 +1,17 @@ +mod assertions; +mod common; mod enums; +#[cfg(feature = "functions")] mod func; mod structs; mod tuple_structs; mod typed; mod values; +pub(crate) use assertions::impl_assertions; +pub(crate) use common::{common_partial_reflect_methods, impl_full_reflect}; pub(crate) use enums::impl_enum; +#[cfg(feature = "functions")] pub(crate) use func::impl_function_traits; pub(crate) use structs::impl_struct; pub(crate) use tuple_structs::impl_tuple_struct; diff --git a/crates/bevy_reflect/derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs index 52f5b96d5a2128..15b11d4dd97a18 100644 --- a/crates/bevy_reflect/derive/src/impls/structs.rs +++ b/crates/bevy_reflect/derive/src/impls/structs.rs @@ -1,7 +1,7 @@ -use crate::impls::{impl_function_traits, impl_type_path, impl_typed}; -use crate::utility::ident_or_index; +use crate::impls::{common_partial_reflect_methods, impl_full_reflect, impl_type_path, impl_typed}; +use crate::struct_utility::FieldAccessors; use crate::ReflectStruct; -use bevy_macro_utils::fq_std::{FQAny, FQBox, FQDefault, FQOption, FQResult}; +use bevy_macro_utils::fq_std::{FQBox, FQDefault, FQOption, FQResult}; use quote::{quote, ToTokens}; /// Implements `Struct`, `GetTypeRegistration`, and `Reflect` for the given derive data. @@ -22,28 +22,14 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS .unwrap_or_else(|| field.declaration_index.to_string()) }) .collect::>(); - let field_idents = reflect_struct - .active_fields() - .map(|field| ident_or_index(field.data.ident.as_ref(), field.declaration_index)) - .collect::>(); - let field_count = field_idents.len(); - let field_indices = (0..field_count).collect::>(); - let hash_fn = reflect_struct - .meta() - .attrs() - .get_hash_impl(bevy_reflect_path); - let debug_fn = reflect_struct.meta().attrs().get_debug_impl(); - let partial_eq_fn = reflect_struct.meta() - .attrs() - .get_partial_eq_impl(bevy_reflect_path) - .unwrap_or_else(|| { - quote! { - fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - #bevy_reflect_path::struct_partial_eq(self, value) - } - } - }); + let FieldAccessors { + fields_ref, + fields_mut, + field_indices, + field_count, + .. + } = FieldAccessors::new(reflect_struct); let where_clause_options = reflect_struct.where_clause_options(); let typed_impl = impl_typed( @@ -53,8 +39,18 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS ); let type_path_impl = impl_type_path(reflect_struct.meta()); + let full_reflect_impl = impl_full_reflect(reflect_struct.meta(), &where_clause_options); + let common_methods = common_partial_reflect_methods( + reflect_struct.meta(), + || Some(quote!(#bevy_reflect_path::struct_partial_eq)), + || None, + ); - let function_impls = impl_function_traits(reflect_struct.meta(), &where_clause_options); + #[cfg(not(feature = "functions"))] + let function_impls = None::; + #[cfg(feature = "functions")] + let function_impls = + crate::impls::impl_function_traits(reflect_struct.meta(), &where_clause_options); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); @@ -73,33 +69,35 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS #type_path_impl + #full_reflect_impl + #function_impls impl #impl_generics #bevy_reflect_path::Struct for #struct_path #ty_generics #where_reflect_clause { - fn field(&self, name: &str) -> #FQOption<&dyn #bevy_reflect_path::Reflect> { + fn field(&self, name: &str) -> #FQOption<&dyn #bevy_reflect_path::PartialReflect> { match name { - #(#field_names => #fqoption::Some(&self.#field_idents),)* + #(#field_names => #fqoption::Some(#fields_ref),)* _ => #FQOption::None, } } - fn field_mut(&mut self, name: &str) -> #FQOption<&mut dyn #bevy_reflect_path::Reflect> { + fn field_mut(&mut self, name: &str) -> #FQOption<&mut dyn #bevy_reflect_path::PartialReflect> { match name { - #(#field_names => #fqoption::Some(&mut self.#field_idents),)* + #(#field_names => #fqoption::Some(#fields_mut),)* _ => #FQOption::None, } } - fn field_at(&self, index: usize) -> #FQOption<&dyn #bevy_reflect_path::Reflect> { + fn field_at(&self, index: usize) -> #FQOption<&dyn #bevy_reflect_path::PartialReflect> { match index { - #(#field_indices => #fqoption::Some(&self.#field_idents),)* + #(#field_indices => #fqoption::Some(#fields_ref),)* _ => #FQOption::None, } } - fn field_at_mut(&mut self, index: usize) -> #FQOption<&mut dyn #bevy_reflect_path::Reflect> { + fn field_at_mut(&mut self, index: usize) -> #FQOption<&mut dyn #bevy_reflect_path::PartialReflect> { match index { - #(#field_indices => #fqoption::Some(&mut self.#field_idents),)* + #(#field_indices => #fqoption::Some(#fields_mut),)* _ => #FQOption::None, } } @@ -121,72 +119,40 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS fn clone_dynamic(&self) -> #bevy_reflect_path::DynamicStruct { let mut dynamic: #bevy_reflect_path::DynamicStruct = #FQDefault::default(); - dynamic.set_represented_type(#bevy_reflect_path::Reflect::get_represented_type_info(self)); - #(dynamic.insert_boxed(#field_names, #bevy_reflect_path::Reflect::clone_value(&self.#field_idents));)* + dynamic.set_represented_type(#bevy_reflect_path::PartialReflect::get_represented_type_info(self)); + #(dynamic.insert_boxed(#field_names, #bevy_reflect_path::PartialReflect::clone_value(#fields_ref));)* dynamic } } - impl #impl_generics #bevy_reflect_path::Reflect for #struct_path #ty_generics #where_reflect_clause { + impl #impl_generics #bevy_reflect_path::PartialReflect for #struct_path #ty_generics #where_reflect_clause { #[inline] fn get_represented_type_info(&self) -> #FQOption<&'static #bevy_reflect_path::TypeInfo> { #FQOption::Some(::type_info()) } #[inline] - fn into_any(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_any(&self) -> &dyn #FQAny { - self - } - - #[inline] - fn as_any_mut(&mut self) -> &mut dyn #FQAny { - self - } - - #[inline] - fn into_reflect(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_reflect(&self) -> &dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn clone_value(&self) -> #FQBox { + fn clone_value(&self) -> #FQBox { #FQBox::new(#bevy_reflect_path::Struct::clone_dynamic(self)) } #[inline] - fn set(&mut self, value: #FQBox) -> #FQResult<(), #FQBox> { - *self = ::take(value)?; - #FQResult::Ok(()) - } - - #[inline] - fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { - if let #bevy_reflect_path::ReflectRef::Struct(struct_value) = #bevy_reflect_path::Reflect::reflect_ref(value) { + fn try_apply( + &mut self, + value: &dyn #bevy_reflect_path::PartialReflect + ) -> #FQResult<(), #bevy_reflect_path::ApplyError> { + if let #bevy_reflect_path::ReflectRef::Struct(struct_value) + = #bevy_reflect_path::PartialReflect::reflect_ref(value) { for (i, value) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::Struct::iter_fields(struct_value)) { let name = #bevy_reflect_path::Struct::name_at(struct_value, i).unwrap(); if let #FQOption::Some(v) = #bevy_reflect_path::Struct::field_mut(self, name) { - #bevy_reflect_path::Reflect::try_apply(v, value)?; + #bevy_reflect_path::PartialReflect::try_apply(v, value)?; } } } else { return #FQResult::Err( #bevy_reflect_path::ApplyError::MismatchedKinds { - from_kind: #bevy_reflect_path::Reflect::reflect_kind(value), + from_kind: #bevy_reflect_path::PartialReflect::reflect_kind(value), to_kind: #bevy_reflect_path::ReflectKind::Struct } ); @@ -210,11 +176,7 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS #bevy_reflect_path::ReflectOwned::Struct(self) } - #hash_fn - - #partial_eq_fn - - #debug_fn + #common_methods } } } diff --git a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs index 559f63343c9e71..fc2228e70e4c81 100644 --- a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs @@ -1,8 +1,8 @@ -use crate::impls::{impl_function_traits, impl_type_path, impl_typed}; +use crate::impls::{common_partial_reflect_methods, impl_full_reflect, impl_type_path, impl_typed}; +use crate::struct_utility::FieldAccessors; use crate::ReflectStruct; -use bevy_macro_utils::fq_std::{FQAny, FQBox, FQDefault, FQOption, FQResult}; +use bevy_macro_utils::fq_std::{FQBox, FQDefault, FQOption, FQResult}; use quote::{quote, ToTokens}; -use syn::{Index, Member}; /// Implements `TupleStruct`, `GetTypeRegistration`, and `Reflect` for the given derive data. pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenStream { @@ -11,33 +11,17 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: let bevy_reflect_path = reflect_struct.meta().bevy_reflect_path(); let struct_path = reflect_struct.meta().type_path(); - let field_idents = reflect_struct - .active_fields() - .map(|field| Member::Unnamed(Index::from(field.declaration_index))) - .collect::>(); - let field_count = field_idents.len(); - let field_indices = (0..field_count).collect::>(); + let FieldAccessors { + fields_ref, + fields_mut, + field_indices, + field_count, + .. + } = FieldAccessors::new(reflect_struct); let where_clause_options = reflect_struct.where_clause_options(); let get_type_registration_impl = reflect_struct.get_type_registration(&where_clause_options); - let hash_fn = reflect_struct - .meta() - .attrs() - .get_hash_impl(bevy_reflect_path); - let debug_fn = reflect_struct.meta().attrs().get_debug_impl(); - let partial_eq_fn = reflect_struct - .meta() - .attrs() - .get_partial_eq_impl(bevy_reflect_path) - .unwrap_or_else(|| { - quote! { - fn reflect_partial_eq(&self, value: &dyn #bevy_reflect_path::Reflect) -> #FQOption { - #bevy_reflect_path::tuple_struct_partial_eq(self, value) - } - } - }); - let typed_impl = impl_typed( reflect_struct.meta(), &where_clause_options, @@ -45,8 +29,18 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: ); let type_path_impl = impl_type_path(reflect_struct.meta()); + let full_reflect_impl = impl_full_reflect(reflect_struct.meta(), &where_clause_options); + let common_methods = common_partial_reflect_methods( + reflect_struct.meta(), + || Some(quote!(#bevy_reflect_path::tuple_struct_partial_eq)), + || None, + ); - let function_impls = impl_function_traits(reflect_struct.meta(), &where_clause_options); + #[cfg(not(feature = "functions"))] + let function_impls = None::; + #[cfg(feature = "functions")] + let function_impls = + crate::impls::impl_function_traits(reflect_struct.meta(), &where_clause_options); let (impl_generics, ty_generics, where_clause) = reflect_struct .meta() @@ -63,19 +57,21 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: #type_path_impl + #full_reflect_impl + #function_impls impl #impl_generics #bevy_reflect_path::TupleStruct for #struct_path #ty_generics #where_reflect_clause { - fn field(&self, index: usize) -> #FQOption<&dyn #bevy_reflect_path::Reflect> { + fn field(&self, index: usize) -> #FQOption<&dyn #bevy_reflect_path::PartialReflect> { match index { - #(#field_indices => #fqoption::Some(&self.#field_idents),)* + #(#field_indices => #fqoption::Some(#fields_ref),)* _ => #FQOption::None, } } - fn field_mut(&mut self, index: usize) -> #FQOption<&mut dyn #bevy_reflect_path::Reflect> { + fn field_mut(&mut self, index: usize) -> #FQOption<&mut dyn #bevy_reflect_path::PartialReflect> { match index { - #(#field_indices => #fqoption::Some(&mut self.#field_idents),)* + #(#field_indices => #fqoption::Some(#fields_mut),)* _ => #FQOption::None, } } @@ -90,71 +86,38 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: fn clone_dynamic(&self) -> #bevy_reflect_path::DynamicTupleStruct { let mut dynamic: #bevy_reflect_path::DynamicTupleStruct = #FQDefault::default(); - dynamic.set_represented_type(#bevy_reflect_path::Reflect::get_represented_type_info(self)); - #(dynamic.insert_boxed(#bevy_reflect_path::Reflect::clone_value(&self.#field_idents));)* + dynamic.set_represented_type(#bevy_reflect_path::PartialReflect::get_represented_type_info(self)); + #(dynamic.insert_boxed(#bevy_reflect_path::PartialReflect::clone_value(#fields_ref));)* dynamic } } - impl #impl_generics #bevy_reflect_path::Reflect for #struct_path #ty_generics #where_reflect_clause { + impl #impl_generics #bevy_reflect_path::PartialReflect for #struct_path #ty_generics #where_reflect_clause { #[inline] fn get_represented_type_info(&self) -> #FQOption<&'static #bevy_reflect_path::TypeInfo> { #FQOption::Some(::type_info()) } - - #[inline] - fn into_any(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_any(&self) -> &dyn #FQAny { - self - } - - #[inline] - fn as_any_mut(&mut self) -> &mut dyn #FQAny { - self - } - - #[inline] - fn into_reflect(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_reflect(&self) -> &dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn #bevy_reflect_path::Reflect { - self - } - #[inline] - fn clone_value(&self) -> #FQBox { + fn clone_value(&self) -> #FQBox { #FQBox::new(#bevy_reflect_path::TupleStruct::clone_dynamic(self)) } #[inline] - fn set(&mut self, value: #FQBox) -> #FQResult<(), #FQBox> { - *self = ::take(value)?; - #FQResult::Ok(()) - } - - #[inline] - fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { - if let #bevy_reflect_path::ReflectRef::TupleStruct(struct_value) = #bevy_reflect_path::Reflect::reflect_ref(value) { + fn try_apply( + &mut self, + value: &dyn #bevy_reflect_path::PartialReflect + ) -> #FQResult<(), #bevy_reflect_path::ApplyError> { + if let #bevy_reflect_path::ReflectRef::TupleStruct(struct_value) = + #bevy_reflect_path::PartialReflect::reflect_ref(value) { for (i, value) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::TupleStruct::iter_fields(struct_value)) { if let #FQOption::Some(v) = #bevy_reflect_path::TupleStruct::field_mut(self, i) { - #bevy_reflect_path::Reflect::try_apply(v, value)?; + #bevy_reflect_path::PartialReflect::try_apply(v, value)?; } } } else { return #FQResult::Err( #bevy_reflect_path::ApplyError::MismatchedKinds { - from_kind: #bevy_reflect_path::Reflect::reflect_kind(value), + from_kind: #bevy_reflect_path::PartialReflect::reflect_kind(value), to_kind: #bevy_reflect_path::ReflectKind::TupleStruct, } ); @@ -178,11 +141,7 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: #bevy_reflect_path::ReflectOwned::TupleStruct(self) } - #hash_fn - - #partial_eq_fn - - #debug_fn + #common_methods } } } diff --git a/crates/bevy_reflect/derive/src/impls/values.rs b/crates/bevy_reflect/derive/src/impls/values.rs index b7eedca2122a8e..3a804f3bed5a2f 100644 --- a/crates/bevy_reflect/derive/src/impls/values.rs +++ b/crates/bevy_reflect/derive/src/impls/values.rs @@ -1,7 +1,7 @@ -use crate::impls::{impl_function_traits, impl_type_path, impl_typed}; +use crate::impls::{common_partial_reflect_methods, impl_full_reflect, impl_type_path, impl_typed}; use crate::utility::WhereClauseOptions; use crate::ReflectMeta; -use bevy_macro_utils::fq_std::{FQAny, FQBox, FQClone, FQOption, FQResult}; +use bevy_macro_utils::fq_std::{FQBox, FQClone, FQOption, FQResult}; use quote::quote; /// Implements `GetTypeRegistration` and `Reflect` for the given type data. @@ -9,10 +9,6 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { let bevy_reflect_path = meta.bevy_reflect_path(); let type_path = meta.type_path(); - let hash_fn = meta.attrs().get_hash_impl(bevy_reflect_path); - let partial_eq_fn = meta.attrs().get_partial_eq_impl(bevy_reflect_path); - let debug_fn = meta.attrs().get_debug_impl(); - #[cfg(feature = "documentation")] let with_docs = { let doc = quote::ToTokens::to_token_stream(meta.doc()); @@ -32,8 +28,30 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { ); let type_path_impl = impl_type_path(meta); + let full_reflect_impl = impl_full_reflect(meta, &where_clause_options); + let common_methods = common_partial_reflect_methods(meta, || None, || None); + + let apply_impl = if let Some(remote_ty) = meta.remote_ty() { + let ty = remote_ty.type_path(); + quote! { + if let #FQOption::Some(value) = ::try_downcast_ref::<#ty>(value) { + *self = Self(#FQClone::clone(value)); + return #FQResult::Ok(()); + } + } + } else { + quote! { + if let #FQOption::Some(value) = ::try_downcast_ref::(value) { + *self = #FQClone::clone(value); + return #FQResult::Ok(()); + } + } + }; - let function_impls = impl_function_traits(meta, &where_clause_options); + #[cfg(not(feature = "functions"))] + let function_impls = None::; + #[cfg(feature = "functions")] + let function_impls = crate::impls::impl_function_traits(meta, &where_clause_options); let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl(); let where_reflect_clause = where_clause_options.extend_where_clause(where_clause); @@ -46,69 +64,34 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { #typed_impl + #full_reflect_impl + #function_impls - impl #impl_generics #bevy_reflect_path::Reflect for #type_path #ty_generics #where_reflect_clause { + impl #impl_generics #bevy_reflect_path::PartialReflect for #type_path #ty_generics #where_reflect_clause { #[inline] fn get_represented_type_info(&self) -> #FQOption<&'static #bevy_reflect_path::TypeInfo> { #FQOption::Some(::type_info()) } #[inline] - fn into_any(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_any(&self) -> &dyn #FQAny { - self - } - - #[inline] - fn as_any_mut(&mut self) -> &mut dyn #FQAny { - self - } - - #[inline] - fn into_reflect(self: #FQBox) -> #FQBox { - self - } - - #[inline] - fn as_reflect(&self) -> &dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn #bevy_reflect_path::Reflect { - self - } - - #[inline] - fn clone_value(&self) -> #FQBox { + fn clone_value(&self) -> #FQBox { #FQBox::new(#FQClone::clone(self)) } #[inline] - fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { - let any = #bevy_reflect_path::Reflect::as_any(value); - if let #FQOption::Some(value) = ::downcast_ref::(any) { - *self = #FQClone::clone(value); - } else { - return #FQResult::Err( - #bevy_reflect_path::ApplyError::MismatchedTypes { - from_type: ::core::convert::Into::into(#bevy_reflect_path::DynamicTypePath::reflect_type_path(value)), - to_type: ::core::convert::Into::into(::type_path()), - } - ); - } - #FQResult::Ok(()) - } - - #[inline] - fn set(&mut self, value: #FQBox) -> #FQResult<(), #FQBox> { - *self = ::take(value)?; - #FQResult::Ok(()) + fn try_apply( + &mut self, + value: &dyn #bevy_reflect_path::PartialReflect + ) -> #FQResult<(), #bevy_reflect_path::ApplyError> { + #apply_impl + + #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedTypes { + from_type: ::core::convert::Into::into(#bevy_reflect_path::DynamicTypePath::reflect_type_path(value)), + to_type: ::core::convert::Into::into(::type_path()), + } + ) } #[inline] @@ -131,11 +114,7 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { #bevy_reflect_path::ReflectOwned::Value(self) } - #hash_fn - - #partial_eq_fn - - #debug_fn + #common_methods } } } diff --git a/crates/bevy_reflect/derive/src/lib.rs b/crates/bevy_reflect/derive/src/lib.rs index 86e0069a887fba..2fdb055b60040f 100644 --- a/crates/bevy_reflect/derive/src/lib.rs +++ b/crates/bevy_reflect/derive/src/lib.rs @@ -27,7 +27,9 @@ mod from_reflect; mod impls; mod reflect_value; mod registration; +mod remote; mod serialization; +mod struct_utility; mod trait_reflection; mod type_path; mod utility; @@ -62,6 +64,8 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre Err(err) => return err.into_compile_error().into(), }; + let assertions = impls::impl_assertions(&derive_data); + let (reflect_impls, from_reflect_impl) = match derive_data { ReflectDerive::Struct(struct_data) | ReflectDerive::UnitStruct(struct_data) => ( impls::impl_struct(&struct_data), @@ -100,7 +104,10 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre TokenStream::from(quote! { const _: () = { #reflect_impls + #from_reflect_impl + + #assertions }; }) } @@ -511,6 +518,91 @@ pub fn reflect_trait(args: TokenStream, input: TokenStream) -> TokenStream { trait_reflection::reflect_trait(&args, input) } +/// Generates a wrapper type that can be used to "derive `Reflect`" for remote types. +/// +/// This works by wrapping the remote type in a generated wrapper that has the `#[repr(transparent)]` attribute. +/// This allows the two types to be safely [transmuted] back-and-forth. +/// +/// # Defining the Wrapper +/// +/// Before defining the wrapper type, please note that it is _required_ that all fields of the remote type are public. +/// The generated code will, at times, need to access or mutate them, +/// and we do not currently have a way to assign getters/setters to each field +/// (but this may change in the future). +/// +/// The wrapper definition should match the remote type 1-to-1. +/// This includes the naming and ordering of the fields and variants. +/// +/// Generics and lifetimes do _not_ need to have the same names, however, they _do_ need to follow the same order. +/// Additionally, whether generics are inlined or placed in a where clause should not matter. +/// +/// Lastly, all macros and doc-comments should be placed __below__ this attribute. +/// If they are placed above, they will not be properly passed to the generated wrapper type. +/// +/// # Example +/// +/// Given a remote type, `RemoteType`: +/// +/// ``` +/// #[derive(Default)] +/// struct RemoteType +/// where +/// T: Default + Clone, +/// { +/// pub foo: T, +/// pub bar: usize +/// } +/// ``` +/// +/// We would define our wrapper type as such: +/// +/// ```ignore +/// use external_crate::RemoteType; +/// +/// #[reflect_remote(RemoteType)] +/// #[derive(Default)] +/// pub struct WrapperType { +/// pub foo: T, +/// pub bar: usize +/// } +/// ``` +/// +/// Apart from all the reflection trait implementations, this generates something like the following: +/// +/// ```ignore +/// use external_crate::RemoteType; +/// +/// #[derive(Default)] +/// #[repr(transparent)] +/// pub struct Wrapper(RemoteType); +/// ``` +/// +/// # Usage as a Field +/// +/// You can tell `Reflect` to use a remote type's wrapper internally on fields of a struct or enum. +/// This allows the real type to be used as usual while `Reflect` handles everything internally. +/// To do this, add the `#[reflect(remote = path::to::MyType)]` attribute to your field: +/// +/// ```ignore +/// #[derive(Reflect)] +/// struct SomeStruct { +/// #[reflect(remote = RemoteTypeWrapper)] +/// data: RemoteType +/// } +/// ``` +/// +/// ## Safety +/// +/// When using the `#[reflect(remote = path::to::MyType)]` field attribute, be sure you are defining the correct wrapper type. +/// Internally, this field will be unsafely [transmuted], and is only sound if using a wrapper generated for the remote type. +/// This also means keeping your wrapper definitions up-to-date with the remote types. +/// +/// [transmuted]: std::mem::transmute +#[proc_macro_attribute] +pub fn reflect_remote(args: TokenStream, input: TokenStream) -> TokenStream { + remote::reflect_remote(args, input) +} + /// A macro used to generate reflection trait implementations for the given type. /// /// This is functionally the same as [deriving `Reflect`] using the `#[reflect_value]` container attribute. diff --git a/crates/bevy_reflect/derive/src/remote.rs b/crates/bevy_reflect/derive/src/remote.rs new file mode 100644 index 00000000000000..9a60f930cf992f --- /dev/null +++ b/crates/bevy_reflect/derive/src/remote.rs @@ -0,0 +1,499 @@ +use crate::derive_data::{ReflectImplSource, ReflectProvenance, ReflectTraitToImpl}; +use crate::impls::impl_assertions; +use crate::utility::ident_or_index; +use crate::{ + from_reflect, impls, ReflectDerive, REFLECT_ATTRIBUTE_NAME, REFLECT_VALUE_ATTRIBUTE_NAME, +}; +use bevy_macro_utils::fq_std::FQOption; +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::{format_ident, quote, quote_spanned}; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::token::PathSep; +use syn::{ + parse_macro_input, DeriveInput, ExprPath, Generics, Member, PathArguments, Type, TypePath, +}; + +/// Generates the remote wrapper type and implements all the necessary traits. +pub(crate) fn reflect_remote(args: TokenStream, input: TokenStream) -> TokenStream { + let remote_args = match syn::parse::(args) { + Ok(path) => path, + Err(err) => return err.to_compile_error().into(), + }; + + let remote_ty = remote_args.remote_ty; + + let ast = parse_macro_input!(input as DeriveInput); + let wrapper_definition = generate_remote_wrapper(&ast, &remote_ty); + + let mut derive_data = match ReflectDerive::from_input( + &ast, + ReflectProvenance { + source: ReflectImplSource::RemoteReflect, + trait_: ReflectTraitToImpl::Reflect, + }, + ) { + Ok(data) => data, + Err(err) => return err.into_compile_error().into(), + }; + + derive_data.set_remote(Some(RemoteType::new(&remote_ty))); + + let assertions = impl_assertions(&derive_data); + let definition_assertions = generate_remote_definition_assertions(&derive_data); + + let reflect_remote_impl = impl_reflect_remote(&derive_data, &remote_ty); + + let (reflect_impls, from_reflect_impl) = match derive_data { + ReflectDerive::Struct(struct_data) | ReflectDerive::UnitStruct(struct_data) => ( + impls::impl_struct(&struct_data), + if struct_data.meta().from_reflect().should_auto_derive() { + Some(from_reflect::impl_struct(&struct_data)) + } else { + None + }, + ), + ReflectDerive::TupleStruct(struct_data) => ( + impls::impl_tuple_struct(&struct_data), + if struct_data.meta().from_reflect().should_auto_derive() { + Some(from_reflect::impl_tuple_struct(&struct_data)) + } else { + None + }, + ), + ReflectDerive::Enum(enum_data) => ( + impls::impl_enum(&enum_data), + if enum_data.meta().from_reflect().should_auto_derive() { + Some(from_reflect::impl_enum(&enum_data)) + } else { + None + }, + ), + ReflectDerive::Value(meta) => ( + impls::impl_value(&meta), + if meta.from_reflect().should_auto_derive() { + Some(from_reflect::impl_value(&meta)) + } else { + None + }, + ), + }; + + TokenStream::from(quote! { + #wrapper_definition + + const _: () = { + #reflect_remote_impl + + #reflect_impls + + #from_reflect_impl + + #definition_assertions + + #assertions + }; + }) +} + +/// Generates the remote wrapper type. +/// +/// # Example +/// +/// If the supplied remote type is `Bar`, then the wrapper type— named `Foo`— would look like: +/// +/// ``` +/// # struct Bar(T); +/// +/// #[repr(transparent)] +/// struct Foo(Bar); +/// ``` +fn generate_remote_wrapper(input: &DeriveInput, remote_ty: &TypePath) -> proc_macro2::TokenStream { + let ident = &input.ident; + let vis = &input.vis; + let ty_generics = &input.generics; + let where_clause = &input.generics.where_clause; + let attrs = input.attrs.iter().filter(|attr| { + !attr.path().is_ident(REFLECT_ATTRIBUTE_NAME) + && !attr.path().is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) + }); + + quote! { + #(#attrs)* + #[repr(transparent)] + #[doc(hidden)] + #vis struct #ident #ty_generics (pub #remote_ty) #where_clause; + } +} + +/// Generates the implementation of the `ReflectRemote` trait for the given derive data and remote type. +/// +/// # Note to Developers +/// +/// The `ReflectRemote` trait could likely be made with default method implementations. +/// However, this makes it really easy for a user to accidentally implement this trait in an unsafe way. +/// To prevent this, we instead generate the implementation through a macro using this function. +fn impl_reflect_remote(input: &ReflectDerive, remote_ty: &TypePath) -> proc_macro2::TokenStream { + let bevy_reflect_path = input.meta().bevy_reflect_path(); + + let type_path = input.meta().type_path(); + let (impl_generics, ty_generics, where_clause) = + input.meta().type_path().generics().split_for_impl(); + + let where_reflect_clause = input + .where_clause_options() + .extend_where_clause(where_clause); + + quote! { + // SAFE: The generated wrapper type is guaranteed to be valid and repr(transparent) over the remote type. + impl #impl_generics #bevy_reflect_path::ReflectRemote for #type_path #ty_generics #where_reflect_clause { + type Remote = #remote_ty; + + fn as_remote(&self) -> &Self::Remote { + &self.0 + } + fn as_remote_mut(&mut self) -> &mut Self::Remote { + &mut self.0 + } + fn into_remote(self) -> Self::Remote + { + // SAFE: The wrapper type should be repr(transparent) over the remote type + unsafe { + // Unfortunately, we have to use `transmute_copy` to avoid a compiler error: + // ``` + // error[E0512]: cannot transmute between types of different sizes, or dependently-sized types + // | + // | std::mem::transmute::(a) + // | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // | + // = note: source type: `A` (this type does not have a fixed size) + // = note: target type: `B` (this type does not have a fixed size) + // ``` + ::core::mem::transmute_copy::( + // `ManuallyDrop` is used to prevent double-dropping `self` + &::core::mem::ManuallyDrop::new(self) + ) + } + } + + fn as_wrapper(remote: &Self::Remote) -> &Self { + // SAFE: The wrapper type should be repr(transparent) over the remote type + unsafe { ::core::mem::transmute::<&Self::Remote, &Self>(remote) } + } + fn as_wrapper_mut(remote: &mut Self::Remote) -> &mut Self { + // SAFE: The wrapper type should be repr(transparent) over the remote type + unsafe { ::core::mem::transmute::<&mut Self::Remote, &mut Self>(remote) } + } + fn into_wrapper(remote: Self::Remote) -> Self + { + // SAFE: The wrapper type should be repr(transparent) over the remote type + unsafe { + // Unfortunately, we have to use `transmute_copy` to avoid a compiler error: + // ``` + // error[E0512]: cannot transmute between types of different sizes, or dependently-sized types + // | + // | std::mem::transmute::(a) + // | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + // | + // = note: source type: `A` (this type does not have a fixed size) + // = note: target type: `B` (this type does not have a fixed size) + // ``` + ::core::mem::transmute_copy::( + // `ManuallyDrop` is used to prevent double-dropping `self` + &::core::mem::ManuallyDrop::new(remote) + ) + } + } + } + } +} + +/// Generates compile-time assertions for remote fields. +/// +/// This prevents types from using an invalid remote type. +/// this works by generating a struct, `RemoteFieldAssertions`, with methods that +/// will result in compile-time failure if types are mismatched. +/// The output of this function is best placed within an anonymous context to maintain hygiene. +/// +/// # Example +/// +/// The following would fail to compile due to an incorrect `#[reflect(remote = ...)]` value. +/// +/// ```ignore +/// mod external_crate { +/// pub struct TheirFoo(pub u32); +/// pub struct TheirBar(pub i32); +/// } +/// +/// #[reflect_remote(external_crate::TheirFoo)] +/// struct MyFoo(pub u32); +/// #[reflect_remote(external_crate::TheirBar)] +/// struct MyBar(pub i32); +/// +/// #[derive(Reflect)] +/// struct MyStruct { +/// #[reflect(remote = MyBar)] // ERROR: expected type `TheirFoo` but found struct `TheirBar` +/// foo: external_crate::TheirFoo +/// } +/// ``` +pub(crate) fn generate_remote_assertions( + derive_data: &ReflectDerive, +) -> Option { + struct RemoteAssertionData<'a> { + ident: Member, + variant: Option<&'a Ident>, + ty: &'a Type, + generics: &'a Generics, + remote_ty: &'a Type, + } + + let bevy_reflect_path = derive_data.meta().bevy_reflect_path(); + + let fields: Box> = match derive_data { + ReflectDerive::Struct(data) + | ReflectDerive::TupleStruct(data) + | ReflectDerive::UnitStruct(data) => Box::new(data.active_fields().filter_map(|field| { + field + .attrs + .remote + .as_ref() + .map(|remote_ty| RemoteAssertionData { + ident: ident_or_index(field.data.ident.as_ref(), field.declaration_index), + variant: None, + ty: &field.data.ty, + generics: data.meta().type_path().generics(), + remote_ty, + }) + })), + ReflectDerive::Enum(data) => Box::new(data.variants().iter().flat_map(|variant| { + variant.active_fields().filter_map(|field| { + field + .attrs + .remote + .as_ref() + .map(|remote_ty| RemoteAssertionData { + ident: ident_or_index(field.data.ident.as_ref(), field.declaration_index), + variant: Some(&variant.data.ident), + ty: &field.data.ty, + generics: data.meta().type_path().generics(), + remote_ty, + }) + }) + })), + + _ => return None, + }; + + let assertions = fields + .map(move |field| { + let ident = if let Some(variant) = field.variant { + format_ident!("{}__{}", variant, field.ident) + } else { + match field.ident { + Member::Named(ident) => ident, + Member::Unnamed(index) => format_ident!("field_{}", index), + } + }; + let (impl_generics, _, where_clause) = field.generics.split_for_impl(); + + let where_reflect_clause = derive_data + .where_clause_options() + .extend_where_clause(where_clause); + + let ty = &field.ty; + let remote_ty = field.remote_ty; + let assertion_ident = format_ident!("assert__{}__is_valid_remote", ident); + + let span = create_assertion_span(remote_ty.span()); + + quote_spanned! {span=> + #[allow(non_snake_case)] + #[allow(clippy::multiple_bound_locations)] + fn #assertion_ident #impl_generics () #where_reflect_clause { + let _: <#remote_ty as #bevy_reflect_path::ReflectRemote>::Remote = (|| -> #FQOption<#ty> { + None + })().unwrap(); + } + } + }) + .collect::(); + + if assertions.is_empty() { + None + } else { + Some(quote! { + struct RemoteFieldAssertions; + + impl RemoteFieldAssertions { + #assertions + } + }) + } +} + +/// Generates compile-time assertions that ensure a remote wrapper definition matches up with the +/// remote type it's wrapping. +/// +/// Note: This currently results in "backwards" error messages like: +/// +/// ```ignore +/// expected: +/// found: +/// ``` +/// +/// Ideally it would be the other way around, but there's no easy way of doing this without +/// generating a copy of the struct/enum definition and using that as the base instead of the remote type. +fn generate_remote_definition_assertions(derive_data: &ReflectDerive) -> proc_macro2::TokenStream { + let meta = derive_data.meta(); + let self_ident = format_ident!("__remote__"); + let self_ty = derive_data.remote_ty().unwrap().type_path(); + let self_expr_path = derive_data.remote_ty().unwrap().as_expr_path().unwrap(); + let (impl_generics, _, where_clause) = meta.type_path().generics().split_for_impl(); + + let where_reflect_clause = derive_data + .where_clause_options() + .extend_where_clause(where_clause); + + let assertions = match derive_data { + ReflectDerive::Struct(data) + | ReflectDerive::TupleStruct(data) + | ReflectDerive::UnitStruct(data) => { + let mut output = proc_macro2::TokenStream::new(); + + for field in data.fields() { + let field_member = + ident_or_index(field.data.ident.as_ref(), field.declaration_index); + let field_ty = &field.data.ty; + let span = create_assertion_span(field_ty.span()); + + output.extend(quote_spanned! {span=> + #self_ident.#field_member = (|| -> #FQOption<#field_ty> {None})().unwrap(); + }); + } + + output + } + ReflectDerive::Enum(data) => { + let variants = data.variants().iter().map(|variant| { + let ident = &variant.data.ident; + + let mut output = proc_macro2::TokenStream::new(); + + if variant.fields().is_empty() { + return quote!(#self_expr_path::#ident => {}); + } + + for field in variant.fields() { + let field_member = + ident_or_index(field.data.ident.as_ref(), field.declaration_index); + let field_ident = format_ident!("field_{}", field_member); + let field_ty = &field.data.ty; + let span = create_assertion_span(field_ty.span()); + + output.extend(quote_spanned! {span=> + #self_expr_path::#ident {#field_member: mut #field_ident, ..} => { + #field_ident = (|| -> #FQOption<#field_ty> {None})().unwrap(); + } + }); + } + + output + }); + + quote! { + match #self_ident { + #(#variants)* + } + } + } + ReflectDerive::Value(_) => { + // No assertions needed since there are no fields to check + proc_macro2::TokenStream::new() + } + }; + + quote! { + const _: () = { + #[allow(non_snake_case)] + #[allow(unused_variables)] + #[allow(unused_assignments)] + #[allow(unreachable_patterns)] + #[allow(clippy::multiple_bound_locations)] + fn assert_wrapper_definition_matches_remote_type #impl_generics (mut #self_ident: #self_ty) #where_reflect_clause { + #assertions + } + }; + } +} + +/// Creates a span located around the given one, but resolves to the assertion's context. +/// +/// This should allow the compiler to point back to the line and column in the user's code, +/// while still attributing the error to the macro. +fn create_assertion_span(span: Span) -> Span { + Span::call_site().located_at(span) +} + +/// A reflected type's remote type. +/// +/// This is a wrapper around [`TypePath`] that allows it to be paired with other remote-specific logic. +#[derive(Copy, Clone)] +pub(crate) struct RemoteType<'a> { + path: &'a TypePath, +} + +impl<'a> RemoteType<'a> { + pub fn new(path: &'a TypePath) -> Self { + Self { path } + } + + /// Returns the [type path](TypePath) of this remote type. + pub fn type_path(&self) -> &'a TypePath { + self.path + } + + /// Attempts to convert the [type path](TypePath) of this remote type into an [expression path](ExprPath). + /// + /// For example, this would convert `foo::Bar` into `foo::Bar::` to be used as part of an expression. + /// + /// This will return an error for types that are parenthesized, such as in `Fn() -> Foo`. + pub fn as_expr_path(&self) -> Result { + let mut expr_path = self.path.clone(); + if let Some(segment) = expr_path.path.segments.last_mut() { + match &mut segment.arguments { + PathArguments::None => {} + PathArguments::AngleBracketed(arg) => { + arg.colon2_token = Some(PathSep::default()); + } + PathArguments::Parenthesized(arg) => { + return Err(syn::Error::new( + arg.span(), + "cannot use parenthesized type as remote type", + )) + } + } + } + + Ok(ExprPath { + path: expr_path.path, + qself: expr_path.qself, + attrs: Vec::new(), + }) + } +} + +/// Metadata from the arguments defined in the `reflect_remote` attribute. +/// +/// The syntax for the arguments is: `#[reflect_remote(REMOTE_TYPE_PATH)]`. +struct RemoteArgs { + remote_ty: TypePath, +} + +impl Parse for RemoteArgs { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { + remote_ty: input.parse()?, + }) + } +} diff --git a/crates/bevy_reflect/derive/src/struct_utility.rs b/crates/bevy_reflect/derive/src/struct_utility.rs new file mode 100644 index 00000000000000..2320e8b86f5b57 --- /dev/null +++ b/crates/bevy_reflect/derive/src/struct_utility.rs @@ -0,0 +1,80 @@ +use crate::derive_data::StructField; +use crate::{utility, ReflectStruct}; +use quote::quote; + +/// A helper struct for creating remote-aware field accessors. +/// +/// These are "remote-aware" because when a field is a remote field, it uses a [`transmute`] internally +/// to access the field. +/// +/// [`transmute`]: core::mem::transmute +pub(crate) struct FieldAccessors { + /// The referenced field accessors, such as `&self.foo`. + pub fields_ref: Vec, + /// The mutably referenced field accessors, such as `&mut self.foo`. + pub fields_mut: Vec, + /// The ordered set of field indices (basically just the range of [0, `field_count`). + pub field_indices: Vec, + /// The number of fields in the reflected struct. + pub field_count: usize, +} + +impl FieldAccessors { + pub fn new(reflect_struct: &ReflectStruct) -> Self { + let bevy_reflect_path = reflect_struct.meta().bevy_reflect_path(); + let fields_ref = Self::get_fields(reflect_struct, |field, accessor| { + match &field.attrs.remote { + Some(wrapper_ty) => { + quote! { + <#wrapper_ty as #bevy_reflect_path::ReflectRemote>::as_wrapper(&#accessor) + } + } + None => quote!(& #accessor), + } + }); + let fields_mut = Self::get_fields(reflect_struct, |field, accessor| { + match &field.attrs.remote { + Some(wrapper_ty) => { + quote! { + <#wrapper_ty as #bevy_reflect_path::ReflectRemote>::as_wrapper_mut(&mut #accessor) + } + } + None => quote!(&mut #accessor), + } + }); + + let field_count = fields_ref.len(); + let field_indices = (0..field_count).collect(); + + Self { + fields_ref, + fields_mut, + field_indices, + field_count, + } + } + + fn get_fields( + reflect_struct: &ReflectStruct, + mut wrapper_fn: F, + ) -> Vec + where + F: FnMut(&StructField, proc_macro2::TokenStream) -> proc_macro2::TokenStream, + { + let is_remote = reflect_struct.meta().is_remote_wrapper(); + reflect_struct + .active_fields() + .map(|field| { + let member = + utility::ident_or_index(field.data.ident.as_ref(), field.declaration_index); + let accessor = if is_remote { + quote!(self.0.#member) + } else { + quote!(self.#member) + }; + + wrapper_fn(field, accessor) + }) + .collect::>() + } +} diff --git a/crates/bevy_reflect/derive/src/utility.rs b/crates/bevy_reflect/derive/src/utility.rs index a12a0a08d5cd8b..15194d0fa7af0b 100644 --- a/crates/bevy_reflect/derive/src/utility.rs +++ b/crates/bevy_reflect/derive/src/utility.rs @@ -94,7 +94,7 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { /// The default bounds added are as follows: /// - `Self` has the bounds `Any + Send + Sync` /// - Type parameters have the bound `TypePath` unless `#[reflect(type_path = false)]` is present - /// - Active fields have the bounds `TypePath` and either `Reflect` if `#[reflect(from_reflect = false)]` is present + /// - Active fields have the bounds `TypePath` and either `PartialReflect` if `#[reflect(from_reflect = false)]` is present /// or `FromReflect` otherwise (or no bounds at all if `#[reflect(no_field_bounds)]` is present) /// /// When the derive is used with `#[reflect(where)]`, the bounds specified in the attribute are added as well. @@ -150,21 +150,19 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { /// // Custom bounds /// T: MyTrait, /// ``` - pub fn extend_where_clause( - &self, - where_clause: Option<&WhereClause>, - ) -> proc_macro2::TokenStream { - let type_path = self.meta.type_path(); - let (_, ty_generics, _) = self.meta.type_path().generics().split_for_impl(); + pub fn extend_where_clause(&self, where_clause: Option<&WhereClause>) -> TokenStream { + // We would normally just use `Self`, but that won't work for generating things like assertion functions + // and trait impls for a type's reference (e.g. `impl FromArg for &MyType`) + let this = self.meta.type_path().true_type(); let required_bounds = self.required_bounds(); // Maintain existing where clause, if any. let mut generic_where_clause = if let Some(where_clause) = where_clause { let predicates = where_clause.predicates.iter(); - quote! {where #type_path #ty_generics: #required_bounds, #(#predicates,)*} + quote! {where #this: #required_bounds, #(#predicates,)*} } else { - quote!(where #type_path #ty_generics: #required_bounds,) + quote!(where #this: #required_bounds,) }; // Add additional reflection trait bounds @@ -227,20 +225,23 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { quote!( #ty : #reflect_bound + #bevy_reflect_path::TypePath + // Needed for `Typed` impls + + #bevy_reflect_path::MaybeTyped + // Needed for `GetTypeRegistration` impls + #bevy_reflect_path::__macro_exports::RegisterForReflection ) })) } } - /// The `Reflect` or `FromReflect` bound to use based on `#[reflect(from_reflect = false)]`. + /// The `PartialReflect` or `FromReflect` bound to use based on `#[reflect(from_reflect = false)]`. fn reflect_bound(&self) -> TokenStream { let bevy_reflect_path = self.meta.bevy_reflect_path(); if self.meta.from_reflect().should_auto_derive() { quote!(#bevy_reflect_path::FromReflect) } else { - quote!(#bevy_reflect_path::Reflect) + quote!(#bevy_reflect_path::PartialReflect) } } @@ -255,7 +256,7 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { } /// The minimum required bounds for a type to be reflected. - fn required_bounds(&self) -> proc_macro2::TokenStream { + fn required_bounds(&self) -> TokenStream { quote!(#FQAny + #FQSend + #FQSync) } } @@ -301,7 +302,7 @@ impl ResultSifter { } /// Turns an `Option` into a `TokenStream` for an `Option`. -pub(crate) fn wrap_in_option(tokens: Option) -> proc_macro2::TokenStream { +pub(crate) fn wrap_in_option(tokens: Option) -> TokenStream { match tokens { Some(tokens) => quote! { #FQOption::Some(#tokens) @@ -320,11 +321,11 @@ pub(crate) enum StringExpr { /// This is either a string literal like `"mystring"`, /// or a string created by a macro like [`module_path`] /// or [`concat`]. - Const(proc_macro2::TokenStream), + Const(TokenStream), /// A [string slice](str) that is borrowed for a `'static` lifetime. - Borrowed(proc_macro2::TokenStream), + Borrowed(TokenStream), /// An [owned string](String). - Owned(proc_macro2::TokenStream), + Owned(TokenStream), } impl From for StringExpr { @@ -353,7 +354,7 @@ impl StringExpr { /// The returned expression will allocate unless the [`StringExpr`] is [already owned]. /// /// [already owned]: StringExpr::Owned - pub fn into_owned(self) -> proc_macro2::TokenStream { + pub fn into_owned(self) -> TokenStream { match self { Self::Const(tokens) | Self::Borrowed(tokens) => quote! { ::std::string::ToString::to_string(#tokens) @@ -363,7 +364,7 @@ impl StringExpr { } /// Returns tokens for a statically borrowed [string slice](str). - pub fn into_borrowed(self) -> proc_macro2::TokenStream { + pub fn into_borrowed(self) -> TokenStream { match self { Self::Const(tokens) | Self::Borrowed(tokens) => tokens, Self::Owned(owned) => quote! { diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 6db72df19354e2..a7d6ef8828aff3 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -1,7 +1,6 @@ -use crate::func::macros::impl_function_traits; use crate::{ - self as bevy_reflect, utility::reflect_hasher, ApplyError, Reflect, ReflectKind, ReflectMut, - ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, utility::reflect_hasher, ApplyError, MaybeTyped, PartialReflect, Reflect, + ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, }; use bevy_reflect_derive::impl_type_path; use std::{ @@ -29,13 +28,13 @@ use std::{ /// # Example /// /// ``` -/// use bevy_reflect::{Reflect, Array}; +/// use bevy_reflect::{PartialReflect, Array}; /// /// let foo: &dyn Array = &[123_u32, 456_u32, 789_u32]; /// assert_eq!(foo.len(), 3); /// -/// let field: &dyn Reflect = foo.get(0).unwrap(); -/// assert_eq!(field.downcast_ref::(), Some(&123)); +/// let field: &dyn PartialReflect = foo.get(0).unwrap(); +/// assert_eq!(field.try_downcast_ref::(), Some(&123)); /// ``` /// /// [array-like]: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-array-type @@ -45,12 +44,12 @@ use std::{ /// [`GetTypeRegistration`]: crate::GetTypeRegistration /// [limitation]: https://github.com/serde-rs/serde/issues/1937 /// [`Deserialize`]: ::serde::Deserialize -pub trait Array: Reflect { +pub trait Array: PartialReflect { /// Returns a reference to the element at `index`, or `None` if out of bounds. - fn get(&self, index: usize) -> Option<&dyn Reflect>; + fn get(&self, index: usize) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the element at `index`, or `None` if out of bounds. - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect>; + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect>; /// Returns the number of elements in the array. fn len(&self) -> usize; @@ -64,13 +63,13 @@ pub trait Array: Reflect { fn iter(&self) -> ArrayIter; /// Drain the elements of this array to get a vector of owned values. - fn drain(self: Box) -> Vec>; + fn drain(self: Box) -> Vec>; /// Clones the list, producing a [`DynamicArray`]. fn clone_dynamic(&self) -> DynamicArray { DynamicArray { represented_type: self.get_represented_type_info(), - values: self.iter().map(Reflect::clone_value).collect(), + values: self.iter().map(PartialReflect::clone_value).collect(), } } } @@ -80,6 +79,7 @@ pub trait Array: Reflect { pub struct ArrayInfo { type_path: TypePathTable, type_id: TypeId, + item_info: fn() -> Option<&'static TypeInfo>, item_type_path: TypePathTable, item_type_id: TypeId, capacity: usize, @@ -94,10 +94,13 @@ impl ArrayInfo { /// /// * `capacity`: The maximum capacity of the underlying array. /// - pub fn new(capacity: usize) -> Self { + pub fn new( + capacity: usize, + ) -> Self { Self { type_path: TypePathTable::of::(), type_id: TypeId::of::(), + item_info: TItem::maybe_type_info, item_type_path: TypePathTable::of::(), item_type_id: TypeId::of::(), capacity, @@ -144,6 +147,14 @@ impl ArrayInfo { TypeId::of::() == self.type_id } + /// The [`TypeInfo`] of the array item. + /// + /// Returns `None` if the array item does not contain static type information, + /// such as for dynamic types. + pub fn item_info(&self) -> Option<&'static TypeInfo> { + (self.item_info)() + } + /// A representation of the type path of the array item. /// /// Provides dynamic access to all methods on [`TypePath`]. @@ -180,27 +191,21 @@ impl ArrayInfo { #[derive(Debug)] pub struct DynamicArray { pub(crate) represented_type: Option<&'static TypeInfo>, - pub(crate) values: Box<[Box]>, + pub(crate) values: Box<[Box]>, } impl DynamicArray { #[inline] - pub fn new(values: Box<[Box]>) -> Self { + pub fn new(values: Box<[Box]>) -> Self { Self { represented_type: None, values, } } - pub fn from_vec(values: Vec) -> Self { - Self { - represented_type: None, - values: values - .into_iter() - .map(|field| Box::new(field) as Box) - .collect::>() - .into_boxed_slice(), - } + #[deprecated(since = "0.15.0", note = "use from_iter")] + pub fn from_vec(values: Vec) -> Self { + Self::from_iter(values) } /// Sets the [type] to be represented by this `DynamicArray`. @@ -223,56 +228,47 @@ impl DynamicArray { } } -impl Reflect for DynamicArray { +impl PartialReflect for DynamicArray { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } - fn apply(&mut self, value: &dyn Reflect) { + fn apply(&mut self, value: &dyn PartialReflect) { array_apply(self, value); } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { array_try_apply(self, value) } - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - #[inline] fn reflect_kind(&self) -> ReflectKind { ReflectKind::Array @@ -294,7 +290,7 @@ impl Reflect for DynamicArray { } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } @@ -303,7 +299,7 @@ impl Reflect for DynamicArray { array_hash(self) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { array_partial_eq(self, value) } @@ -321,12 +317,12 @@ impl Reflect for DynamicArray { impl Array for DynamicArray { #[inline] - fn get(&self, index: usize) -> Option<&dyn Reflect> { + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { self.values.get(index).map(|value| &**value) } #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { self.values.get_mut(index).map(|value| &mut **value) } @@ -341,7 +337,7 @@ impl Array for DynamicArray { } #[inline] - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { self.values.into_vec() } @@ -358,8 +354,44 @@ impl Array for DynamicArray { } } +impl FromIterator> for DynamicArray { + fn from_iter>>(values: I) -> Self { + Self { + represented_type: None, + values: values.into_iter().collect::>().into_boxed_slice(), + } + } +} + +impl FromIterator for DynamicArray { + fn from_iter>(values: I) -> Self { + values + .into_iter() + .map(|value| Box::new(value).into_partial_reflect()) + .collect() + } +} + +impl IntoIterator for DynamicArray { + type Item = Box; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.values.into_vec().into_iter() + } +} + +impl<'a> IntoIterator for &'a DynamicArray { + type Item = &'a dyn PartialReflect; + type IntoIter = ArrayIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + impl_type_path!((in bevy_reflect) DynamicArray); -impl_function_traits!(DynamicArray); + /// An iterator over an [`Array`]. pub struct ArrayIter<'a> { array: &'a dyn Array, @@ -375,7 +407,7 @@ impl<'a> ArrayIter<'a> { } impl<'a> Iterator for ArrayIter<'a> { - type Item = &'a dyn Reflect; + type Item = &'a dyn PartialReflect; #[inline] fn next(&mut self) -> Option { @@ -395,7 +427,7 @@ impl<'a> ExactSizeIterator for ArrayIter<'a> {} /// Returns the `u64` hash of the given [array](Array). #[inline] -pub fn array_hash(array: &A) -> Option { +pub fn array_hash(array: &A) -> Option { let mut hasher = reflect_hasher(); Any::type_id(array).hash(&mut hasher); array.len().hash(&mut hasher); @@ -413,7 +445,7 @@ pub fn array_hash(array: &A) -> Option { /// * Panics if the reflected value is not a [valid array](ReflectRef::Array). /// #[inline] -pub fn array_apply(array: &mut A, reflect: &dyn Reflect) { +pub fn array_apply(array: &mut A, reflect: &dyn PartialReflect) { if let ReflectRef::Array(reflect_array) = reflect.reflect_ref() { if array.len() != reflect_array.len() { panic!("Attempted to apply different sized `Array` types."); @@ -438,7 +470,10 @@ pub fn array_apply(array: &mut A, reflect: &dyn Reflect) { /// * Returns any error that is generated while applying elements to each other. /// #[inline] -pub fn array_try_apply(array: &mut A, reflect: &dyn Reflect) -> Result<(), ApplyError> { +pub fn array_try_apply( + array: &mut A, + reflect: &dyn PartialReflect, +) -> Result<(), ApplyError> { if let ReflectRef::Array(reflect_array) = reflect.reflect_ref() { if array.len() != reflect_array.len() { return Err(ApplyError::DifferentSize { @@ -464,7 +499,10 @@ pub fn array_try_apply(array: &mut A, reflect: &dyn Reflect) -> Result /// /// Returns [`None`] if the comparison couldn't even be performed. #[inline] -pub fn array_partial_eq(array: &A, reflect: &dyn Reflect) -> Option { +pub fn array_partial_eq( + array: &A, + reflect: &dyn PartialReflect, +) -> Option { match reflect.reflect_ref() { ReflectRef::Array(reflect_array) if reflect_array.len() == array.len() => { for (a, b) in array.iter().zip(reflect_array.iter()) { @@ -498,7 +536,7 @@ pub fn array_partial_eq(array: &A, reflect: &dyn Reflect) -> Option) -> std::fmt::Result { +pub fn array_debug(dyn_array: &dyn Array, f: &mut Formatter<'_>) -> std::fmt::Result { let mut debug = f.debug_list(); for item in dyn_array.iter() { debug.entry(&item as &dyn Debug); diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 9b3dea9601aa9a..bf1c1fa6ddb203 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -1,12 +1,11 @@ use bevy_reflect_derive::impl_type_path; -use crate::func::macros::impl_function_traits; use crate::{ self as bevy_reflect, enum_debug, enum_hash, enum_partial_eq, ApplyError, DynamicStruct, - DynamicTuple, Enum, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, Tuple, - TypeInfo, VariantFieldIter, VariantType, + DynamicTuple, Enum, PartialReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, + Struct, Tuple, TypeInfo, VariantFieldIter, VariantType, }; -use std::any::Any; + use std::fmt::Formatter; /// A dynamic representation of an enum variant. @@ -53,7 +52,7 @@ impl From<()> for DynamicVariant { /// # Example /// /// ``` -/// # use bevy_reflect::{DynamicEnum, DynamicVariant, Reflect}; +/// # use bevy_reflect::{DynamicEnum, DynamicVariant, Reflect, PartialReflect}; /// /// // The original enum value /// let mut value: Option = Some(123); @@ -65,7 +64,7 @@ impl From<()> for DynamicVariant { /// ); /// /// // Apply the DynamicEnum as a patch to the original value -/// value.apply(&dyn_enum); +/// value.apply(dyn_enum.as_partial_reflect()); /// /// // Tada! /// assert_eq!(None, value); @@ -202,7 +201,7 @@ impl DynamicEnum { } impl Enum for DynamicEnum { - fn field(&self, name: &str) -> Option<&dyn Reflect> { + fn field(&self, name: &str) -> Option<&dyn PartialReflect> { if let DynamicVariant::Struct(data) = &self.variant { data.field(name) } else { @@ -210,7 +209,7 @@ impl Enum for DynamicEnum { } } - fn field_at(&self, index: usize) -> Option<&dyn Reflect> { + fn field_at(&self, index: usize) -> Option<&dyn PartialReflect> { if let DynamicVariant::Tuple(data) = &self.variant { data.field(index) } else { @@ -218,7 +217,7 @@ impl Enum for DynamicEnum { } } - fn field_mut(&mut self, name: &str) -> Option<&mut dyn Reflect> { + fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect> { if let DynamicVariant::Struct(data) = &mut self.variant { data.field_mut(name) } else { @@ -226,7 +225,7 @@ impl Enum for DynamicEnum { } } - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { if let DynamicVariant::Tuple(data) = &mut self.variant { data.field_mut(index) } else { @@ -288,44 +287,41 @@ impl Enum for DynamicEnum { } } -impl Reflect for DynamicEnum { +impl PartialReflect for DynamicEnum { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } #[inline] - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { if let ReflectRef::Enum(value) = value.reflect_ref() { if Enum::variant_name(self) == value.variant_name() { // Same variant -> just update fields @@ -378,12 +374,6 @@ impl Reflect for DynamicEnum { Ok(()) } - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - #[inline] fn reflect_kind(&self) -> ReflectKind { ReflectKind::Enum @@ -405,7 +395,7 @@ impl Reflect for DynamicEnum { } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } @@ -415,7 +405,7 @@ impl Reflect for DynamicEnum { } #[inline] - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { enum_partial_eq(self, value) } @@ -425,7 +415,11 @@ impl Reflect for DynamicEnum { enum_debug(self, f)?; write!(f, ")") } + + #[inline] + fn is_dynamic(&self) -> bool { + true + } } impl_type_path!((in bevy_reflect) DynamicEnum); -impl_function_traits!(DynamicEnum); diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index a81dc501486608..993f08a7c110ff 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -1,5 +1,5 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; -use crate::{DynamicEnum, Reflect, TypePath, TypePathTable, VariantInfo, VariantType}; +use crate::{DynamicEnum, PartialReflect, TypePath, TypePathTable, VariantInfo, VariantType}; use bevy_utils::HashMap; use std::any::{Any, TypeId}; use std::slice::Iter; @@ -89,19 +89,19 @@ use std::sync::Arc; /// [`None`]: Option::None /// [`Some`]: Option::Some /// [`Reflect`]: bevy_reflect_derive::Reflect -pub trait Enum: Reflect { +pub trait Enum: PartialReflect { /// Returns a reference to the value of the field (in the current variant) with the given name. /// /// For non-[`VariantType::Struct`] variants, this should return `None`. - fn field(&self, name: &str) -> Option<&dyn Reflect>; + fn field(&self, name: &str) -> Option<&dyn PartialReflect>; /// Returns a reference to the value of the field (in the current variant) at the given index. - fn field_at(&self, index: usize) -> Option<&dyn Reflect>; + fn field_at(&self, index: usize) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the value of the field (in the current variant) with the given name. /// /// For non-[`VariantType::Struct`] variants, this should return `None`. - fn field_mut(&mut self, name: &str) -> Option<&mut dyn Reflect>; + fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect>; /// Returns a mutable reference to the value of the field (in the current variant) at the given index. - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn Reflect>; + fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect>; /// Returns the index of the field (in the current variant) with the given name. /// /// For non-[`VariantType::Struct`] variants, this should return `None`. @@ -307,8 +307,8 @@ impl<'a> Iterator for VariantFieldIter<'a> { impl<'a> ExactSizeIterator for VariantFieldIter<'a> {} pub enum VariantField<'a> { - Struct(&'a str, &'a dyn Reflect), - Tuple(&'a dyn Reflect), + Struct(&'a str, &'a dyn PartialReflect), + Tuple(&'a dyn PartialReflect), } impl<'a> VariantField<'a> { @@ -320,7 +320,7 @@ impl<'a> VariantField<'a> { } } - pub fn value(&self) -> &'a dyn Reflect { + pub fn value(&self) -> &'a dyn PartialReflect { match *self { Self::Struct(_, value) | Self::Tuple(value) => value, } diff --git a/crates/bevy_reflect/src/enums/helpers.rs b/crates/bevy_reflect/src/enums/helpers.rs index 09a3516c79d46b..6771b1b1655aba 100644 --- a/crates/bevy_reflect/src/enums/helpers.rs +++ b/crates/bevy_reflect/src/enums/helpers.rs @@ -1,4 +1,5 @@ -use crate::{utility::reflect_hasher, Enum, Reflect, ReflectRef, VariantType}; +use crate::PartialReflect; +use crate::{utility::reflect_hasher, Enum, ReflectRef, VariantType}; use std::fmt::Debug; use std::hash::{Hash, Hasher}; @@ -15,16 +16,16 @@ pub fn enum_hash(value: &TEnum) -> Option { Some(hasher.finish()) } -/// Compares an [`Enum`] with a [`Reflect`] value. +/// Compares an [`Enum`] with a [`PartialReflect`] value. /// /// Returns true if and only if all of the following are true: /// - `b` is an enum; /// - `b` is the same variant as `a`; /// - For each field in `a`, `b` contains a field with the same name and -/// [`Reflect::reflect_partial_eq`] returns `Some(true)` for the two field +/// [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for the two field /// values. #[inline] -pub fn enum_partial_eq(a: &TEnum, b: &dyn Reflect) -> Option { +pub fn enum_partial_eq(a: &TEnum, b: &dyn PartialReflect) -> Option { // Both enums? let ReflectRef::Enum(b) = b.reflect_ref() else { return Some(false); diff --git a/crates/bevy_reflect/src/enums/mod.rs b/crates/bevy_reflect/src/enums/mod.rs index 2cb77af571fcd1..95a94e68e97e1c 100644 --- a/crates/bevy_reflect/src/enums/mod.rs +++ b/crates/bevy_reflect/src/enums/mod.rs @@ -50,6 +50,18 @@ mod tests { if let VariantInfo::Tuple(variant) = info.variant("B").unwrap() { assert!(variant.field_at(0).unwrap().is::()); assert!(variant.field_at(1).unwrap().is::()); + assert!(variant + .field_at(0) + .unwrap() + .type_info() + .unwrap() + .is::()); + assert!(variant + .field_at(1) + .unwrap() + .type_info() + .unwrap() + .is::()); } else { panic!("Expected `VariantInfo::Tuple`"); } @@ -60,6 +72,12 @@ mod tests { if let VariantInfo::Struct(variant) = info.variant("C").unwrap() { assert!(variant.field_at(0).unwrap().is::()); assert!(variant.field("foo").unwrap().is::()); + assert!(variant + .field("foo") + .unwrap() + .type_info() + .unwrap() + .is::()); } else { panic!("Expected `VariantInfo::Struct`"); } @@ -193,6 +211,12 @@ mod tests { assert_eq!(MyEnum::A, value); } + #[test] + fn dynamic_enum_should_return_is_dynamic() { + let dyn_enum = DynamicEnum::from(MyEnum::B(123, 321)); + assert!(dyn_enum.is_dynamic()); + } + #[test] fn enum_should_iterate_fields() { // === Unit === // @@ -596,71 +620,71 @@ mod tests { C2 { value: f32 }, } - let a: &dyn Reflect = &TestEnum::A; - let b: &dyn Reflect = &TestEnum::A; + let a: &dyn PartialReflect = &TestEnum::A; + let b: &dyn PartialReflect = &TestEnum::A; assert!( a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::A == TestEnum::A" ); - let a: &dyn Reflect = &TestEnum::A; - let b: &dyn Reflect = &TestEnum::A1; + let a: &dyn PartialReflect = &TestEnum::A; + let b: &dyn PartialReflect = &TestEnum::A1; assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::A != TestEnum::A1" ); - let a: &dyn Reflect = &TestEnum::B(123); - let b: &dyn Reflect = &TestEnum::B(123); + let a: &dyn PartialReflect = &TestEnum::B(123); + let b: &dyn PartialReflect = &TestEnum::B(123); assert!( a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::B(123) == TestEnum::B(123)" ); - let a: &dyn Reflect = &TestEnum::B(123); - let b: &dyn Reflect = &TestEnum::B(321); + let a: &dyn PartialReflect = &TestEnum::B(123); + let b: &dyn PartialReflect = &TestEnum::B(321); assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::B(123) != TestEnum::B(321)" ); - let a: &dyn Reflect = &TestEnum::B(123); - let b: &dyn Reflect = &TestEnum::B1(123); + let a: &dyn PartialReflect = &TestEnum::B(123); + let b: &dyn PartialReflect = &TestEnum::B1(123); assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::B(123) != TestEnum::B1(123)" ); - let a: &dyn Reflect = &TestEnum::B(123); - let b: &dyn Reflect = &TestEnum::B2(123, 123); + let a: &dyn PartialReflect = &TestEnum::B(123); + let b: &dyn PartialReflect = &TestEnum::B2(123, 123); assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::B(123) != TestEnum::B2(123, 123)" ); - let a: &dyn Reflect = &TestEnum::C { value: 123 }; - let b: &dyn Reflect = &TestEnum::C { value: 123 }; + let a: &dyn PartialReflect = &TestEnum::C { value: 123 }; + let b: &dyn PartialReflect = &TestEnum::C { value: 123 }; assert!( a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::C{{value: 123}} == TestEnum::C{{value: 123}}" ); - let a: &dyn Reflect = &TestEnum::C { value: 123 }; - let b: &dyn Reflect = &TestEnum::C { value: 321 }; + let a: &dyn PartialReflect = &TestEnum::C { value: 123 }; + let b: &dyn PartialReflect = &TestEnum::C { value: 321 }; assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::C{{value: 123}} != TestEnum::C{{value: 321}}" ); - let a: &dyn Reflect = &TestEnum::C { value: 123 }; - let b: &dyn Reflect = &TestEnum::C1 { value: 123 }; + let a: &dyn PartialReflect = &TestEnum::C { value: 123 }; + let b: &dyn PartialReflect = &TestEnum::C1 { value: 123 }; assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::C{{value: 123}} != TestEnum::C1{{value: 123}}" ); - let a: &dyn Reflect = &TestEnum::C { value: 123 }; - let b: &dyn Reflect = &TestEnum::C2 { value: 1.23 }; + let a: &dyn PartialReflect = &TestEnum::C { value: 123 }; + let b: &dyn PartialReflect = &TestEnum::C2 { value: 1.23 }; assert!( !a.reflect_partial_eq(b).unwrap_or_default(), "expected TestEnum::C{{value: 123}} != TestEnum::C2{{value: 1.23}}" diff --git a/crates/bevy_reflect/src/enums/variants.rs b/crates/bevy_reflect/src/enums/variants.rs index 27dddb09bae9e8..fb6baf652e7439 100644 --- a/crates/bevy_reflect/src/enums/variants.rs +++ b/crates/bevy_reflect/src/enums/variants.rs @@ -2,7 +2,9 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; use crate::{NamedField, UnnamedField}; use bevy_utils::HashMap; use std::slice::Iter; + use std::sync::Arc; +use thiserror::Error; /// Describes the form of an enum variant. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] @@ -35,6 +37,19 @@ pub enum VariantType { Unit, } +/// A [`VariantInfo`]-specific error. +#[derive(Debug, Error)] +pub enum VariantInfoError { + /// Caused when a variant was expected to be of a certain [type], but was not. + /// + /// [type]: VariantType + #[error("variant type mismatch: expected {expected:?}, received {received:?}")] + TypeMismatch { + expected: VariantType, + received: VariantType, + }, +} + /// A container for compile-time enum variant info. #[derive(Clone, Debug)] pub enum VariantInfo { @@ -85,6 +100,17 @@ impl VariantInfo { } } + /// Returns the [type] of this variant. + /// + /// [type]: VariantType + pub fn variant_type(&self) -> VariantType { + match self { + Self::Struct(_) => VariantType::Struct, + Self::Tuple(_) => VariantType::Tuple, + Self::Unit(_) => VariantType::Unit, + } + } + impl_custom_attribute_methods!( self, match self { @@ -96,6 +122,29 @@ impl VariantInfo { ); } +macro_rules! impl_cast_method { + ($name:ident : $kind:ident => $info:ident) => { + #[doc = concat!("Attempts a cast to [`", stringify!($info), "`].")] + #[doc = concat!("\n\nReturns an error if `self` is not [`VariantInfo::", stringify!($kind), "`].")] + pub fn $name(&self) -> Result<&$info, VariantInfoError> { + match self { + Self::$kind(info) => Ok(info), + _ => Err(VariantInfoError::TypeMismatch { + expected: VariantType::$kind, + received: self.variant_type(), + }), + } + } + }; +} + +/// Conversion convenience methods for [`VariantInfo`]. +impl VariantInfo { + impl_cast_method!(as_struct_variant: Struct => StructVariantInfo); + impl_cast_method!(as_tuple_variant: Tuple => TupleVariantInfo); + impl_cast_method!(as_unit_variant: Unit => UnitVariantInfo); +} + /// Type info for struct variants. #[derive(Clone, Debug)] pub struct StructVariantInfo { @@ -304,3 +353,28 @@ impl UnitVariantInfo { impl_custom_attribute_methods!(self.custom_attributes, "variant"); } + +#[cfg(test)] +mod tests { + use super::*; + use crate as bevy_reflect; + use crate::{Reflect, Typed}; + + #[test] + fn should_return_error_on_invalid_cast() { + #[derive(Reflect)] + enum Foo { + Bar, + } + + let info = Foo::type_info().as_enum().unwrap(); + let variant = info.variant_at(0).unwrap(); + assert!(matches!( + variant.as_tuple_variant(), + Err(VariantInfoError::TypeMismatch { + expected: VariantType::Tuple, + received: VariantType::Unit + }) + )); + } +} diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index 31855aeb782286..e20d952ac50978 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -1,5 +1,5 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; -use crate::{Reflect, TypePath, TypePathTable}; +use crate::{MaybeTyped, PartialReflect, TypeInfo, TypePath, TypePathTable}; use std::any::{Any, TypeId}; use std::sync::Arc; @@ -7,6 +7,7 @@ use std::sync::Arc; #[derive(Clone, Debug)] pub struct NamedField { name: &'static str, + type_info: fn() -> Option<&'static TypeInfo>, type_path: TypePathTable, type_id: TypeId, custom_attributes: Arc, @@ -16,9 +17,10 @@ pub struct NamedField { impl NamedField { /// Create a new [`NamedField`]. - pub fn new(name: &'static str) -> Self { + pub fn new(name: &'static str) -> Self { Self { name, + type_info: T::maybe_type_info, type_path: TypePathTable::of::(), type_id: TypeId::of::(), custom_attributes: Arc::new(CustomAttributes::default()), @@ -46,6 +48,15 @@ impl NamedField { self.name } + /// The [`TypeInfo`] of the field. + /// + /// + /// Returns `None` if the field does not contain static type information, + /// such as for dynamic types. + pub fn type_info(&self) -> Option<&'static TypeInfo> { + (self.type_info)() + } + /// A representation of the type path of the field. /// /// Provides dynamic access to all methods on [`TypePath`]. @@ -86,6 +97,7 @@ impl NamedField { #[derive(Clone, Debug)] pub struct UnnamedField { index: usize, + type_info: fn() -> Option<&'static TypeInfo>, type_path: TypePathTable, type_id: TypeId, custom_attributes: Arc, @@ -94,9 +106,10 @@ pub struct UnnamedField { } impl UnnamedField { - pub fn new(index: usize) -> Self { + pub fn new(index: usize) -> Self { Self { index, + type_info: T::maybe_type_info, type_path: TypePathTable::of::(), type_id: TypeId::of::(), custom_attributes: Arc::new(CustomAttributes::default()), @@ -124,6 +137,15 @@ impl UnnamedField { self.index } + /// The [`TypeInfo`] of the field. + /// + /// + /// Returns `None` if the field does not contain static type information, + /// such as for dynamic types. + pub fn type_info(&self) -> Option<&'static TypeInfo> { + (self.type_info)() + } + /// A representation of the type path of the field. /// /// Provides dynamic access to all methods on [`TypePath`]. diff --git a/crates/bevy_reflect/src/from_reflect.rs b/crates/bevy_reflect/src/from_reflect.rs index aacb7d9d2516f3..b1ca70f52894d3 100644 --- a/crates/bevy_reflect/src/from_reflect.rs +++ b/crates/bevy_reflect/src/from_reflect.rs @@ -1,4 +1,4 @@ -use crate::{FromType, Reflect}; +use crate::{FromType, PartialReflect, Reflect}; /// A trait that enables types to be dynamically constructed from reflected data. /// @@ -27,7 +27,7 @@ use crate::{FromType, Reflect}; )] pub trait FromReflect: Reflect + Sized { /// Constructs a concrete instance of `Self` from a reflected value. - fn from_reflect(reflect: &dyn Reflect) -> Option; + fn from_reflect(reflect: &dyn PartialReflect) -> Option; /// Attempts to downcast the given value to `Self` using, /// constructing the value using [`from_reflect`] if that fails. @@ -39,8 +39,10 @@ pub trait FromReflect: Reflect + Sized { /// [`from_reflect`]: Self::from_reflect /// [`DynamicStruct`]: crate::DynamicStruct /// [`DynamicList`]: crate::DynamicList - fn take_from_reflect(reflect: Box) -> Result> { - match reflect.take::() { + fn take_from_reflect( + reflect: Box, + ) -> Result> { + match reflect.try_take::() { Ok(value) => Ok(value), Err(value) => match Self::from_reflect(value.as_ref()) { None => Err(value), @@ -101,7 +103,7 @@ pub trait FromReflect: Reflect + Sized { /// [`DynamicEnum`]: crate::DynamicEnum #[derive(Clone)] pub struct ReflectFromReflect { - from_reflect: fn(&dyn Reflect) -> Option>, + from_reflect: fn(&dyn PartialReflect) -> Option>, } impl ReflectFromReflect { @@ -110,7 +112,7 @@ impl ReflectFromReflect { /// This will convert the object to a concrete type if it wasn't already, and return /// the value as `Box`. #[allow(clippy::wrong_self_convention)] - pub fn from_reflect(&self, reflect_value: &dyn Reflect) -> Option> { + pub fn from_reflect(&self, reflect_value: &dyn PartialReflect) -> Option> { (self.from_reflect)(reflect_value) } } diff --git a/crates/bevy_reflect/src/func/args/arg.rs b/crates/bevy_reflect/src/func/args/arg.rs index a79ab592aab09a..14c02eecf11757 100644 --- a/crates/bevy_reflect/src/func/args/arg.rs +++ b/crates/bevy_reflect/src/func/args/arg.rs @@ -1,80 +1,203 @@ -use crate::func::args::{ArgError, ArgInfo, Ownership}; -use crate::Reflect; +use crate::func::args::{ArgError, FromArg, Ownership}; +use crate::{PartialReflect, Reflect, TypePath}; +use std::ops::Deref; -/// Represents an argument that can be passed to a [`DynamicFunction`]. +/// Represents an argument that can be passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug)] -pub enum Arg<'a> { - Owned(Box), - Ref(&'a dyn Reflect), - Mut(&'a mut dyn Reflect), +pub struct Arg<'a> { + index: usize, + value: ArgValue<'a>, } impl<'a> Arg<'a> { - /// Returns `Ok(T)` if the argument is [`Arg::Owned`]. - pub fn take_owned(self, info: &ArgInfo) -> Result { - match self { - Arg::Owned(arg) => arg.take().map_err(|arg| ArgError::UnexpectedType { - id: info.id().clone(), - expected: ::std::borrow::Cow::Borrowed(info.type_path()), - received: ::std::borrow::Cow::Owned(arg.reflect_type_path().to_string()), + /// Create a new [`Arg`] with the given index and value. + pub fn new(index: usize, value: ArgValue<'a>) -> Self { + Self { index, value } + } + + /// The index of the argument. + pub fn index(&self) -> usize { + self.index + } + + /// Set the index of the argument. + pub(crate) fn set_index(&mut self, index: usize) { + self.index = index; + } + + /// The value of the argument. + pub fn value(&self) -> &ArgValue<'a> { + &self.value + } + + /// Take the value of the argument. + pub fn take_value(self) -> ArgValue<'a> { + self.value + } + + /// Take the value of the argument and attempt to convert it to a concrete value, `T`. + /// + /// This is a convenience method for calling [`FromArg::from_arg`] on the argument. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let a = 1u32; + /// let b = 2u32; + /// let mut c = 3u32; + /// let mut args = ArgList::new().push_owned(a).push_ref(&b).push_mut(&mut c); + /// + /// let a = args.take::().unwrap(); + /// assert_eq!(a, 1); + /// + /// let b = args.take::<&u32>().unwrap(); + /// assert_eq!(*b, 2); + /// + /// let c = args.take::<&mut u32>().unwrap(); + /// assert_eq!(*c, 3); + /// ``` + pub fn take(self) -> Result, ArgError> { + T::from_arg(self) + } + + /// Returns `Ok(T)` if the argument is [`ArgValue::Owned`]. + /// + /// If the argument is not owned, returns an error. + /// + /// It's generally preferred to use [`Self::take`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let value = 123u32; + /// let mut args = ArgList::new().push_owned(value); + /// let value = args.take_owned::().unwrap(); + /// assert_eq!(value, 123); + /// ``` + pub fn take_owned(self) -> Result { + match self.value { + ArgValue::Owned(arg) => arg.try_take().map_err(|arg| ArgError::UnexpectedType { + index: self.index, + expected: std::borrow::Cow::Borrowed(T::type_path()), + received: std::borrow::Cow::Owned(arg.reflect_type_path().to_string()), }), - Arg::Ref(_) => Err(ArgError::InvalidOwnership { - id: info.id().clone(), + ArgValue::Ref(_) => Err(ArgError::InvalidOwnership { + index: self.index, expected: Ownership::Owned, received: Ownership::Ref, }), - Arg::Mut(_) => Err(ArgError::InvalidOwnership { - id: info.id().clone(), + ArgValue::Mut(_) => Err(ArgError::InvalidOwnership { + index: self.index, expected: Ownership::Owned, received: Ownership::Mut, }), } } - /// Returns `Ok(&T)` if the argument is [`Arg::Ref`]. - pub fn take_ref(self, info: &ArgInfo) -> Result<&'a T, ArgError> { - match self { - Arg::Owned(_) => Err(ArgError::InvalidOwnership { - id: info.id().clone(), + /// Returns `Ok(&T)` if the argument is [`ArgValue::Ref`]. + /// + /// If the argument is not a reference, returns an error. + /// + /// It's generally preferred to use [`Self::take`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let value = 123u32; + /// let mut args = ArgList::new().push_ref(&value); + /// let value = args.take_ref::().unwrap(); + /// assert_eq!(*value, 123); + /// ``` + pub fn take_ref(self) -> Result<&'a T, ArgError> { + match self.value { + ArgValue::Owned(_) => Err(ArgError::InvalidOwnership { + index: self.index, expected: Ownership::Ref, received: Ownership::Owned, }), - Arg::Ref(arg) => Ok(arg.downcast_ref().ok_or_else(|| ArgError::UnexpectedType { - id: info.id().clone(), - expected: ::std::borrow::Cow::Borrowed(info.type_path()), - received: ::std::borrow::Cow::Owned(arg.reflect_type_path().to_string()), - })?), - Arg::Mut(_) => Err(ArgError::InvalidOwnership { - id: info.id().clone(), + ArgValue::Ref(arg) => { + Ok(arg + .try_downcast_ref() + .ok_or_else(|| ArgError::UnexpectedType { + index: self.index, + expected: std::borrow::Cow::Borrowed(T::type_path()), + received: std::borrow::Cow::Owned(arg.reflect_type_path().to_string()), + })?) + } + ArgValue::Mut(_) => Err(ArgError::InvalidOwnership { + index: self.index, expected: Ownership::Ref, received: Ownership::Mut, }), } } - /// Returns `Ok(&mut T)` if the argument is [`Arg::Mut`]. - pub fn take_mut(self, info: &ArgInfo) -> Result<&'a mut T, ArgError> { - match self { - Arg::Owned(_) => Err(ArgError::InvalidOwnership { - id: info.id().clone(), + /// Returns `Ok(&mut T)` if the argument is [`ArgValue::Mut`]. + /// + /// If the argument is not a mutable reference, returns an error. + /// + /// It's generally preferred to use [`Self::take`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let mut value = 123u32; + /// let mut args = ArgList::new().push_mut(&mut value); + /// let value = args.take_mut::().unwrap(); + /// assert_eq!(*value, 123); + /// ``` + pub fn take_mut(self) -> Result<&'a mut T, ArgError> { + match self.value { + ArgValue::Owned(_) => Err(ArgError::InvalidOwnership { + index: self.index, expected: Ownership::Mut, received: Ownership::Owned, }), - Arg::Ref(_) => Err(ArgError::InvalidOwnership { - id: info.id().clone(), + ArgValue::Ref(_) => Err(ArgError::InvalidOwnership { + index: self.index, expected: Ownership::Mut, received: Ownership::Ref, }), - Arg::Mut(arg) => { - let received = ::std::borrow::Cow::Owned(arg.reflect_type_path().to_string()); - Ok(arg.downcast_mut().ok_or_else(|| ArgError::UnexpectedType { - id: info.id().clone(), - expected: ::std::borrow::Cow::Borrowed(info.type_path()), - received, - })?) + ArgValue::Mut(arg) => { + let received = std::borrow::Cow::Owned(arg.reflect_type_path().to_string()); + Ok(arg + .try_downcast_mut() + .ok_or_else(|| ArgError::UnexpectedType { + index: self.index, + expected: std::borrow::Cow::Borrowed(T::type_path()), + received, + })?) } } } } + +/// Represents an argument that can be passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut +#[derive(Debug)] +pub enum ArgValue<'a> { + Owned(Box), + Ref(&'a dyn PartialReflect), + Mut(&'a mut dyn PartialReflect), +} + +impl<'a> Deref for ArgValue<'a> { + type Target = dyn PartialReflect; + + fn deref(&self) -> &Self::Target { + match self { + ArgValue::Owned(arg) => arg.as_ref(), + ArgValue::Ref(arg) => *arg, + ArgValue::Mut(arg) => *arg, + } + } +} diff --git a/crates/bevy_reflect/src/func/args/error.rs b/crates/bevy_reflect/src/func/args/error.rs index 83794b81d64b6d..13ebc0a7d3de47 100644 --- a/crates/bevy_reflect/src/func/args/error.rs +++ b/crates/bevy_reflect/src/func/args/error.rs @@ -2,25 +2,30 @@ use alloc::borrow::Cow; use thiserror::Error; -use crate::func::args::{ArgId, Ownership}; +use crate::func::args::Ownership; /// An error that occurs when converting an [argument]. /// -/// [argument]: crate::func::Arg +/// [argument]: crate::func::args::Arg #[derive(Debug, Error, PartialEq)] pub enum ArgError { /// The argument is not the expected type. - #[error("expected `{expected}` but received `{received}` (@ {id:?})")] + #[error("expected `{expected}` but received `{received}` (@ argument index {index})")] UnexpectedType { - id: ArgId, + index: usize, expected: Cow<'static, str>, received: Cow<'static, str>, }, /// The argument has the wrong ownership. - #[error("expected {expected} value but received {received} value (@ {id:?})")] + #[error("expected {expected} value but received {received} value (@ argument index {index})")] InvalidOwnership { - id: ArgId, + index: usize, expected: Ownership, received: Ownership, }, + /// Occurs when attempting to access an argument from an empty [`ArgList`]. + /// + /// [`ArgList`]: crate::func::args::ArgList + #[error("expected an argument but received none")] + EmptyArgList, } diff --git a/crates/bevy_reflect/src/func/args/from_arg.rs b/crates/bevy_reflect/src/func/args/from_arg.rs index 7ae61e6856663f..88d04aefe7525a 100644 --- a/crates/bevy_reflect/src/func/args/from_arg.rs +++ b/crates/bevy_reflect/src/func/args/from_arg.rs @@ -1,23 +1,32 @@ -use crate::func::args::{Arg, ArgError, ArgInfo}; +use crate::func::args::{Arg, ArgError}; /// A trait for types that can be created from an [`Arg`]. /// +/// This trait exists so that types can be automatically converted into an [`Arg`] +/// so they can be put into an [`ArgList`] and passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// /// This trait is used instead of a blanket [`From`] implementation due to coherence issues: /// we can't implement `From` for both `T` and `&T`/`&mut T`. /// /// This trait is automatically implemented when using the `Reflect` [derive macro]. /// +/// [`ArgList`]: crate::func::args::ArgList +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut /// [derive macro]: derive@crate::Reflect pub trait FromArg { - /// The type of the item created from the argument. + /// The type to convert into. /// /// This should almost always be the same as `Self`, but with the lifetime `'a`. - type Item<'a>; + /// + /// The reason we use a separate associated type is to allow for the lifetime + /// to be tied to the argument, rather than the type itself. + type This<'a>; /// Creates an item from an argument. /// /// The argument must be of the expected type and ownership. - fn from_arg<'a>(arg: Arg<'a>, info: &ArgInfo) -> Result, ArgError>; + fn from_arg(arg: Arg) -> Result, ArgError>; } /// Implements the [`FromArg`] trait for the given type. @@ -54,12 +63,9 @@ macro_rules! impl_from_arg { $($U $(: $U1 $(+ $U2)*)?),* )? { - type Item<'from_arg> = $ty; - fn from_arg<'from_arg>( - arg: $crate::func::args::Arg<'from_arg>, - info: &$crate::func::args::ArgInfo, - ) -> Result, $crate::func::args::ArgError> { - arg.take_owned(info) + type This<'from_arg> = $ty; + fn from_arg(arg: $crate::func::args::Arg) -> Result, $crate::func::args::ArgError> { + arg.take_owned() } } @@ -72,12 +78,9 @@ macro_rules! impl_from_arg { $($U $(: $U1 $(+ $U2)*)?),* )? { - type Item<'from_arg> = &'from_arg $ty; - fn from_arg<'from_arg>( - arg: $crate::func::args::Arg<'from_arg>, - info: &$crate::func::args::ArgInfo, - ) -> Result, $crate::func::args::ArgError> { - arg.take_ref(info) + type This<'from_arg> = &'from_arg $ty; + fn from_arg(arg: $crate::func::args::Arg) -> Result, $crate::func::args::ArgError> { + arg.take_ref() } } @@ -90,12 +93,9 @@ macro_rules! impl_from_arg { $($U $(: $U1 $(+ $U2)*)?),* )? { - type Item<'from_arg> = &'from_arg mut $ty; - fn from_arg<'from_arg>( - arg: $crate::func::args::Arg<'from_arg>, - info: &$crate::func::args::ArgInfo, - ) -> Result, $crate::func::args::ArgError> { - arg.take_mut(info) + type This<'from_arg> = &'from_arg mut $ty; + fn from_arg(arg: $crate::func::args::Arg) -> Result, $crate::func::args::ArgError> { + arg.take_mut() } } }; diff --git a/crates/bevy_reflect/src/func/args/info.rs b/crates/bevy_reflect/src/func/args/info.rs index baf85e02079183..e932e77be3f33b 100644 --- a/crates/bevy_reflect/src/func/args/info.rs +++ b/crates/bevy_reflect/src/func/args/info.rs @@ -3,10 +3,11 @@ use alloc::borrow::Cow; use crate::func::args::{GetOwnership, Ownership}; use crate::TypePath; -/// Type information for an [`Arg`] used in a [`DynamicFunction`]. +/// Type information for an [`Arg`] used in a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// -/// [`Arg`]: crate::func::Arg +/// [`Arg`]: crate::func::args::Arg /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct ArgInfo { /// The index of the argument within its function. @@ -54,10 +55,14 @@ impl ArgInfo { /// This is because the name needs to be manually set using [`Self::with_name`] /// since the name can't be inferred from the function type alone. /// - /// For [`DynamicFunctions`] created using [`IntoFunction`], the name will always be `None`. + /// For [`DynamicFunctions`] created using [`IntoFunction`] + /// and [`DynamicFunctionMuts`] created using [`IntoFunctionMut`], + /// the name will always be `None`. /// /// [`DynamicFunctions`]: crate::func::DynamicFunction /// [`IntoFunction`]: crate::func::IntoFunction + /// [`DynamicFunctionMuts`]: crate::func::DynamicFunctionMut + /// [`IntoFunctionMut`]: crate::func::IntoFunctionMut pub fn name(&self) -> Option<&str> { self.name.as_deref() } @@ -67,6 +72,9 @@ impl ArgInfo { self.ownership } + /// The [type path] of the argument. + /// + /// [type path]: TypePath::type_path pub fn type_path(&self) -> &'static str { self.type_path } diff --git a/crates/bevy_reflect/src/func/args/list.rs b/crates/bevy_reflect/src/func/args/list.rs index 984662f3f5c17d..15458250f4d8ef 100644 --- a/crates/bevy_reflect/src/func/args/list.rs +++ b/crates/bevy_reflect/src/func/args/list.rs @@ -1,12 +1,14 @@ -use crate::func::args::Arg; -use crate::Reflect; +use crate::func::args::{Arg, ArgValue, FromArg}; +use crate::func::ArgError; +use crate::{PartialReflect, Reflect, TypePath}; +use std::collections::VecDeque; -/// A list of arguments that can be passed to a [`DynamicFunction`]. +/// A list of arguments that can be passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// # Example /// /// ``` -/// # use bevy_reflect::func::{Arg, ArgList}; +/// # use bevy_reflect::func::{ArgValue, ArgList}; /// let foo = 123; /// let bar = 456; /// let mut baz = 789; @@ -20,62 +22,384 @@ use crate::Reflect; /// // Push a mutable reference argument /// .push_mut(&mut baz) /// // Push a manually constructed argument -/// .push(Arg::Ref(&3.14)); +/// .push_arg(ArgValue::Ref(&3.14)); /// ``` /// +/// [arguments]: Arg /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Default, Debug)] -pub struct ArgList<'a>(Vec>); +pub struct ArgList<'a> { + list: VecDeque>, + /// A flag that indicates if the list needs to be re-indexed. + /// + /// This flag should be set when an argument is removed from the beginning of the list, + /// so that any future push operations will re-index the arguments. + needs_reindex: bool, +} impl<'a> ArgList<'a> { /// Create a new empty list of arguments. pub fn new() -> Self { - Self(Vec::new()) + Self { + list: VecDeque::new(), + needs_reindex: false, + } } - /// Push an [`Arg`] onto the list. - pub fn push(mut self, arg: Arg<'a>) -> Self { - self.0.push(arg); + /// Push an [`ArgValue`] onto the list. + /// + /// If an argument was previously removed from the beginning of the list, + /// this method will also re-index the list. + pub fn push_arg(mut self, arg: ArgValue<'a>) -> Self { + if self.needs_reindex { + for (index, arg) in self.list.iter_mut().enumerate() { + arg.set_index(index); + } + self.needs_reindex = false; + } + + let index = self.list.len(); + self.list.push_back(Arg::new(index, arg)); self } - /// Push an [`Arg::Ref`] onto the list with the given reference. - pub fn push_ref(self, arg: &'a dyn Reflect) -> Self { - self.push(Arg::Ref(arg)) + /// Push an [`ArgValue::Ref`] onto the list with the given reference. + /// + /// If an argument was previously removed from the beginning of the list, + /// this method will also re-index the list. + pub fn push_ref(self, arg: &'a dyn PartialReflect) -> Self { + self.push_arg(ArgValue::Ref(arg)) + } + + /// Push an [`ArgValue::Mut`] onto the list with the given mutable reference. + /// + /// If an argument was previously removed from the beginning of the list, + /// this method will also re-index the list. + pub fn push_mut(self, arg: &'a mut dyn PartialReflect) -> Self { + self.push_arg(ArgValue::Mut(arg)) + } + + /// Push an [`ArgValue::Owned`] onto the list with the given owned value. + /// + /// If an argument was previously removed from the beginning of the list, + /// this method will also re-index the list. + pub fn push_owned(self, arg: impl PartialReflect) -> Self { + self.push_arg(ArgValue::Owned(Box::new(arg))) + } + + /// Push an [`ArgValue::Owned`] onto the list with the given boxed value. + /// + /// If an argument was previously removed from the beginning of the list, + /// this method will also re-index the list. + pub fn push_boxed(self, arg: Box) -> Self { + self.push_arg(ArgValue::Owned(arg)) + } + + /// Remove the first argument in the list and return it. + /// + /// It's generally preferred to use [`Self::take`] instead of this method + /// as it provides a more ergonomic way to immediately downcast the argument. + pub fn take_arg(&mut self) -> Result, ArgError> { + self.needs_reindex = true; + self.list.pop_front().ok_or(ArgError::EmptyArgList) + } + + /// Remove the first argument in the list and return `Ok(T::This)`. + /// + /// If the list is empty or the [`FromArg::from_arg`] call fails, returns an error. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let a = 1u32; + /// let b = 2u32; + /// let mut c = 3u32; + /// let mut args = ArgList::new().push_owned(a).push_ref(&b).push_mut(&mut c); + /// + /// let a = args.take::().unwrap(); + /// assert_eq!(a, 1); + /// + /// let b = args.take::<&u32>().unwrap(); + /// assert_eq!(*b, 2); + /// + /// let c = args.take::<&mut u32>().unwrap(); + /// assert_eq!(*c, 3); + /// ``` + pub fn take(&mut self) -> Result, ArgError> { + self.take_arg()?.take::() + } + + /// Remove the first argument in the list and return `Ok(T)` if the argument is [`ArgValue::Owned`]. + /// + /// If the list is empty or the argument is not owned, returns an error. + /// + /// It's generally preferred to use [`Self::take`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let value = 123u32; + /// let mut args = ArgList::new().push_owned(value); + /// let value = args.take_owned::().unwrap(); + /// assert_eq!(value, 123); + /// ``` + pub fn take_owned(&mut self) -> Result { + self.take_arg()?.take_owned() + } + + /// Remove the first argument in the list and return `Ok(&T)` if the argument is [`ArgValue::Ref`]. + /// + /// If the list is empty or the argument is not a reference, returns an error. + /// + /// It's generally preferred to use [`Self::take`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let value = 123u32; + /// let mut args = ArgList::new().push_ref(&value); + /// let value = args.take_ref::().unwrap(); + /// assert_eq!(*value, 123); + /// ``` + pub fn take_ref(&mut self) -> Result<&'a T, ArgError> { + self.take_arg()?.take_ref() + } + + /// Remove the first argument in the list and return `Ok(&mut T)` if the argument is [`ArgValue::Mut`]. + /// + /// If the list is empty or the argument is not a mutable reference, returns an error. + /// + /// It's generally preferred to use [`Self::take`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let mut value = 123u32; + /// let mut args = ArgList::new().push_mut(&mut value); + /// let value = args.take_mut::().unwrap(); + /// assert_eq!(*value, 123); + /// ``` + pub fn take_mut(&mut self) -> Result<&'a mut T, ArgError> { + self.take_arg()?.take_mut() + } + + /// Remove the last argument in the list and return it. + /// + /// It's generally preferred to use [`Self::pop`] instead of this method + /// as it provides a more ergonomic way to immediately downcast the argument. + pub fn pop_arg(&mut self) -> Result, ArgError> { + self.list.pop_back().ok_or(ArgError::EmptyArgList) } - /// Push an [`Arg::Mut`] onto the list with the given mutable reference. - pub fn push_mut(self, arg: &'a mut dyn Reflect) -> Self { - self.push(Arg::Mut(arg)) + /// Remove the last argument in the list and return `Ok(T::This)`. + /// + /// If the list is empty or the [`FromArg::from_arg`] call fails, returns an error. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let a = 1u32; + /// let b = 2u32; + /// let mut c = 3u32; + /// let mut args = ArgList::new().push_owned(a).push_ref(&b).push_mut(&mut c); + /// + /// let c = args.pop::<&mut u32>().unwrap(); + /// assert_eq!(*c, 3); + /// + /// let b = args.pop::<&u32>().unwrap(); + /// assert_eq!(*b, 2); + /// + /// let a = args.pop::().unwrap(); + /// assert_eq!(a, 1); + /// ``` + pub fn pop(&mut self) -> Result, ArgError> { + self.pop_arg()?.take::() } - /// Push an [`Arg::Owned`] onto the list with the given owned value. - pub fn push_owned(self, arg: impl Reflect) -> Self { - self.push(Arg::Owned(Box::new(arg))) + /// Remove the last argument in the list and return `Ok(T)` if the argument is [`ArgValue::Owned`]. + /// + /// If the list is empty or the argument is not owned, returns an error. + /// + /// It's generally preferred to use [`Self::pop`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let value = 123u32; + /// let mut args = ArgList::new().push_owned(value); + /// let value = args.pop_owned::().unwrap(); + /// assert_eq!(value, 123); + /// ``` + pub fn pop_owned(&mut self) -> Result { + self.pop_arg()?.take_owned() } - /// Push an [`Arg::Owned`] onto the list with the given boxed value. - pub fn push_boxed(self, arg: Box) -> Self { - self.push(Arg::Owned(arg)) + /// Remove the last argument in the list and return `Ok(&T)` if the argument is [`ArgValue::Ref`]. + /// + /// If the list is empty or the argument is not a reference, returns an error. + /// + /// It's generally preferred to use [`Self::pop`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let value = 123u32; + /// let mut args = ArgList::new().push_ref(&value); + /// let value = args.pop_ref::().unwrap(); + /// assert_eq!(*value, 123); + /// ``` + pub fn pop_ref(&mut self) -> Result<&'a T, ArgError> { + self.pop_arg()?.take_ref() } - /// Pop the last argument from the list, if there is one. - pub fn pop(&mut self) -> Option> { - self.0.pop() + /// Remove the last argument in the list and return `Ok(&mut T)` if the argument is [`ArgValue::Mut`]. + /// + /// If the list is empty or the argument is not a mutable reference, returns an error. + /// + /// It's generally preferred to use [`Self::pop`] instead of this method. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::ArgList; + /// let mut value = 123u32; + /// let mut args = ArgList::new().push_mut(&mut value); + /// let value = args.pop_mut::().unwrap(); + /// assert_eq!(*value, 123); + /// ``` + pub fn pop_mut(&mut self) -> Result<&'a mut T, ArgError> { + self.pop_arg()?.take_mut() } /// Returns the number of arguments in the list. pub fn len(&self) -> usize { - self.0.len() + self.list.len() } /// Returns `true` if the list of arguments is empty. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.list.is_empty() } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_push_arguments_in_order() { + let args = ArgList::new() + .push_owned(123) + .push_owned(456) + .push_owned(789); + + assert_eq!(args.len(), 3); + assert_eq!(args.list[0].index(), 0); + assert_eq!(args.list[1].index(), 1); + assert_eq!(args.list[2].index(), 2); + } + + #[test] + fn should_push_arg_with_correct_ownership() { + let a = String::from("a"); + let b = String::from("b"); + let mut c = String::from("c"); + let d = String::from("d"); + let e = String::from("e"); + let f = String::from("f"); + let mut g = String::from("g"); + + let args = ArgList::new() + .push_arg(ArgValue::Owned(Box::new(a))) + .push_arg(ArgValue::Ref(&b)) + .push_arg(ArgValue::Mut(&mut c)) + .push_owned(d) + .push_boxed(Box::new(e)) + .push_ref(&f) + .push_mut(&mut g); + + assert!(matches!(args.list[0].value(), &ArgValue::Owned(_))); + assert!(matches!(args.list[1].value(), &ArgValue::Ref(_))); + assert!(matches!(args.list[2].value(), &ArgValue::Mut(_))); + assert!(matches!(args.list[3].value(), &ArgValue::Owned(_))); + assert!(matches!(args.list[4].value(), &ArgValue::Owned(_))); + assert!(matches!(args.list[5].value(), &ArgValue::Ref(_))); + assert!(matches!(args.list[6].value(), &ArgValue::Mut(_))); + } + + #[test] + fn should_take_args_in_order() { + let a = String::from("a"); + let b = 123_i32; + let c = 456_usize; + let mut d = 5.78_f32; + + let mut args = ArgList::new() + .push_owned(a) + .push_ref(&b) + .push_ref(&c) + .push_mut(&mut d); + + assert_eq!(args.len(), 4); + assert_eq!(args.take_owned::().unwrap(), String::from("a")); + assert_eq!(args.take::<&i32>().unwrap(), &123); + assert_eq!(args.take_ref::().unwrap(), &456); + assert_eq!(args.take_mut::().unwrap(), &mut 5.78); + assert_eq!(args.len(), 0); + } + + #[test] + fn should_pop_args_in_reverse_order() { + let a = String::from("a"); + let b = 123_i32; + let c = 456_usize; + let mut d = 5.78_f32; + + let mut args = ArgList::new() + .push_owned(a) + .push_ref(&b) + .push_ref(&c) + .push_mut(&mut d); + + assert_eq!(args.len(), 4); + assert_eq!(args.pop_mut::().unwrap(), &mut 5.78); + assert_eq!(args.pop_ref::().unwrap(), &456); + assert_eq!(args.pop::<&i32>().unwrap(), &123); + assert_eq!(args.pop_owned::().unwrap(), String::from("a")); + assert_eq!(args.len(), 0); + } + + #[test] + fn should_reindex_on_push_after_take() { + let mut args = ArgList::new() + .push_owned(123) + .push_owned(456) + .push_owned(789); + + assert!(!args.needs_reindex); + + args.take_arg().unwrap(); + assert!(args.needs_reindex); + assert!(args.list[0].value().reflect_partial_eq(&456).unwrap()); + assert_eq!(args.list[0].index(), 1); + assert!(args.list[1].value().reflect_partial_eq(&789).unwrap()); + assert_eq!(args.list[1].index(), 2); - /// Take ownership of the list of arguments. - pub fn take(self) -> Vec> { - self.0 + let args = args.push_owned(123); + assert!(!args.needs_reindex); + assert!(args.list[0].value().reflect_partial_eq(&456).unwrap()); + assert_eq!(args.list[0].index(), 0); + assert!(args.list[1].value().reflect_partial_eq(&789).unwrap()); + assert_eq!(args.list[1].index(), 1); + assert!(args.list[2].value().reflect_partial_eq(&123).unwrap()); + assert_eq!(args.list[2].index(), 2); } } diff --git a/crates/bevy_reflect/src/func/args/mod.rs b/crates/bevy_reflect/src/func/args/mod.rs index adcbc0ec641d0a..da0ea00bb1abdf 100644 --- a/crates/bevy_reflect/src/func/args/mod.rs +++ b/crates/bevy_reflect/src/func/args/mod.rs @@ -1,6 +1,7 @@ -//! Argument types and utilities for working with [`DynamicFunctions`]. +//! Argument types and utilities for working with [`DynamicFunction`] and [`DynamicFunctionMut`]. //! -//! [`DynamicFunctions`]: crate::func::DynamicFunction +//! [`DynamicFunction`]: crate::func::DynamicFunction +//! [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut pub use arg::*; pub use error::*; diff --git a/crates/bevy_reflect/src/func/args/ownership.rs b/crates/bevy_reflect/src/func/args/ownership.rs index 554126d295f67b..b9395c742f4044 100644 --- a/crates/bevy_reflect/src/func/args/ownership.rs +++ b/crates/bevy_reflect/src/func/args/ownership.rs @@ -2,8 +2,14 @@ use core::fmt::{Display, Formatter}; /// A trait for getting the ownership of a type. /// +/// This trait exists so that [`TypedFunction`] can automatically generate +/// [`FunctionInfo`] containing the proper [`Ownership`] for its [argument] types. +/// /// This trait is automatically implemented when using the `Reflect` [derive macro]. /// +/// [`TypedFunction`]: crate::func::TypedFunction +/// [`FunctionInfo`]: crate::func::FunctionInfo +/// [argument]: crate::func::args::Arg /// [derive macro]: derive@crate::Reflect pub trait GetOwnership { /// Returns the ownership of [`Self`]. diff --git a/crates/bevy_reflect/src/func/dynamic_function.rs b/crates/bevy_reflect/src/func/dynamic_function.rs new file mode 100644 index 00000000000000..2794f1cb9ae89b --- /dev/null +++ b/crates/bevy_reflect/src/func/dynamic_function.rs @@ -0,0 +1,232 @@ +use crate::func::args::{ArgInfo, ArgList}; +use crate::func::info::FunctionInfo; +use crate::func::{DynamicFunctionMut, FunctionResult, IntoFunction, IntoFunctionMut, ReturnInfo}; +use alloc::borrow::Cow; +use core::fmt::{Debug, Formatter}; +use std::sync::Arc; + +/// A dynamic representation of a function. +/// +/// This type can be used to represent any callable that satisfies [`Fn`] +/// (or the reflection-based equivalent, [`ReflectFn`]). +/// That is, any function or closure that does not mutably borrow data from its environment. +/// +/// For functions that do need to capture their environment mutably (i.e. mutable closures), +/// see [`DynamicFunctionMut`]. +/// +/// See the [module-level documentation] for more information. +/// +/// You will generally not need to construct this manually. +/// Instead, many functions and closures can be automatically converted using the [`IntoFunction`] trait. +/// +/// # Example +/// +/// Most of the time, a [`DynamicFunction`] can be created using the [`IntoFunction`] trait: +/// +/// ``` +/// # use bevy_reflect::func::{ArgList, DynamicFunction, FunctionInfo, IntoFunction}; +/// # +/// fn add(a: i32, b: i32) -> i32 { +/// a + b +/// } +/// +/// // Convert the function into a dynamic function using `IntoFunction::into_function`: +/// let mut func: DynamicFunction = add.into_function(); +/// +/// // Dynamically call it: +/// let args = ArgList::default().push_owned(25_i32).push_owned(75_i32); +/// let value = func.call(args).unwrap().unwrap_owned(); +/// +/// // Check the result: +/// assert_eq!(value.try_downcast_ref::(), Some(&100)); +/// ``` +/// +/// [`ReflectFn`]: crate::func::ReflectFn +/// [module-level documentation]: crate::func +pub struct DynamicFunction<'env> { + pub(super) info: FunctionInfo, + pub(super) func: Arc Fn(ArgList<'a>) -> FunctionResult<'a> + Send + Sync + 'env>, +} + +impl<'env> DynamicFunction<'env> { + /// Create a new [`DynamicFunction`]. + /// + /// The given function can be used to call out to any other callable, + /// including functions, closures, or methods. + /// + /// It's important that the function signature matches the provided [`FunctionInfo`]. + /// This info may be used by consumers of this function for validation and debugging. + pub fn new Fn(ArgList<'a>) -> FunctionResult<'a> + Send + Sync + 'env>( + func: F, + info: FunctionInfo, + ) -> Self { + Self { + info, + func: Arc::new(func), + } + } + + /// Set the name of the function. + /// + /// For [`DynamicFunctions`] created using [`IntoFunction`], + /// the default name will always be the full path to the function as returned by [`std::any::type_name`], + /// unless the function is a closure, anonymous function, or function pointer, + /// in which case the name will be `None`. + /// + /// [`DynamicFunctions`]: DynamicFunction + pub fn with_name(mut self, name: impl Into>) -> Self { + self.info = self.info.with_name(name); + self + } + + /// Set the argument information of the function. + /// + /// It's important that the arguments match the intended function signature, + /// as this can be used by consumers of this function for validation and debugging. + pub fn with_args(mut self, args: Vec) -> Self { + self.info = self.info.with_args(args); + self + } + + /// Set the return information of the function. + pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self { + self.info = self.info.with_return_info(return_info); + self + } + + /// Call the function with the given arguments. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::{IntoFunction, ArgList}; + /// let c = 23; + /// let add = |a: i32, b: i32| -> i32 { + /// a + b + c + /// }; + /// + /// let mut func = add.into_function().with_name("add"); + /// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + /// let result = func.call(args).unwrap().unwrap_owned(); + /// assert_eq!(result.try_take::().unwrap(), 123); + /// ``` + pub fn call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a> { + (self.func)(args) + } + + /// Returns the function info. + pub fn info(&self) -> &FunctionInfo { + &self.info + } + + /// The [name] of the function. + /// + /// For [`DynamicFunctions`] created using [`IntoFunction`], + /// the default name will always be the full path to the function as returned by [`std::any::type_name`], + /// unless the function is a closure, anonymous function, or function pointer, + /// in which case the name will be `None`. + /// + /// This can be overridden using [`with_name`]. + /// + /// [name]: FunctionInfo::name + /// [`DynamicFunctions`]: DynamicFunction + /// [`with_name`]: Self::with_name + pub fn name(&self) -> Option<&Cow<'static, str>> { + self.info.name() + } +} + +/// Outputs the function's signature. +/// +/// This takes the format: `DynamicFunction(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`. +/// +/// Names for arguments and the function itself are optional and will default to `_` if not provided. +impl<'env> Debug for DynamicFunction<'env> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let name = self.info.name().unwrap_or(&Cow::Borrowed("_")); + write!(f, "DynamicFunction(fn {name}(")?; + + for (index, arg) in self.info.args().iter().enumerate() { + let name = arg.name().unwrap_or("_"); + let ty = arg.type_path(); + write!(f, "{name}: {ty}")?; + + if index + 1 < self.info.args().len() { + write!(f, ", ")?; + } + } + + let ret = self.info.return_info().type_path(); + write!(f, ") -> {ret})") + } +} + +impl<'env> Clone for DynamicFunction<'env> { + fn clone(&self) -> Self { + Self { + info: self.info.clone(), + func: Arc::clone(&self.func), + } + } +} + +impl<'env> IntoFunction<'env, ()> for DynamicFunction<'env> { + #[inline] + fn into_function(self) -> DynamicFunction<'env> { + self + } +} + +impl<'env> IntoFunctionMut<'env, ()> for DynamicFunction<'env> { + #[inline] + fn into_function_mut(self) -> DynamicFunctionMut<'env> { + DynamicFunctionMut::from(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_overwrite_function_name() { + let c = 23; + let func = (|a: i32, b: i32| a + b + c) + .into_function() + .with_name("my_function"); + assert_eq!(func.info().name().unwrap(), "my_function"); + } + + #[test] + fn should_convert_dynamic_function_with_into_function() { + fn make_closure<'env, F: IntoFunction<'env, M>, M>(f: F) -> DynamicFunction<'env> { + f.into_function() + } + + let c = 23; + let function: DynamicFunction = make_closure(|a: i32, b: i32| a + b + c); + let _: DynamicFunction = make_closure(function); + } + + #[test] + fn should_clone_dynamic_function() { + let hello = String::from("Hello"); + + let greet = |name: &String| -> String { format!("{}, {}!", hello, name) }; + + let greet = greet.into_function().with_name("greet"); + let clone = greet.clone(); + + assert_eq!(greet.name().unwrap(), "greet"); + assert_eq!(clone.name().unwrap(), "greet"); + + let clone_value = clone + .call(ArgList::default().push_ref(&String::from("world"))) + .unwrap() + .unwrap_owned() + .try_take::() + .unwrap(); + + assert_eq!(clone_value, "Hello, world!"); + } +} diff --git a/crates/bevy_reflect/src/func/dynamic_function_mut.rs b/crates/bevy_reflect/src/func/dynamic_function_mut.rs new file mode 100644 index 00000000000000..06d1e0f71c4636 --- /dev/null +++ b/crates/bevy_reflect/src/func/dynamic_function_mut.rs @@ -0,0 +1,255 @@ +use alloc::borrow::Cow; +use core::fmt::{Debug, Formatter}; + +use crate::func::args::{ArgInfo, ArgList}; +use crate::func::info::FunctionInfo; +use crate::func::{DynamicFunction, FunctionResult, IntoFunctionMut, ReturnInfo}; + +/// A dynamic representation of a function. +/// +/// This type can be used to represent any callable that satisfies [`FnMut`] +/// (or the reflection-based equivalent, [`ReflectFnMut`]). +/// That is, any function or closure. +/// +/// For functions that do not need to capture their environment mutably, +/// it's recommended to use [`DynamicFunction`] instead. +/// +/// This type can be seen as a superset of [`DynamicFunction`]. +/// +/// See the [module-level documentation] for more information. +/// +/// You will generally not need to construct this manually. +/// Instead, many functions and closures can be automatically converted using the [`IntoFunctionMut`] trait. +/// +/// # Example +/// +/// Most of the time, a [`DynamicFunctionMut`] can be created using the [`IntoFunctionMut`] trait: +/// +/// ``` +/// # use bevy_reflect::func::{ArgList, DynamicFunctionMut, FunctionInfo, IntoFunctionMut}; +/// # +/// let mut list: Vec = vec![1, 2, 3]; +/// +/// // `replace` is a closure that captures a mutable reference to `list` +/// let mut replace = |index: usize, value: i32| -> i32 { +/// let old_value = list[index]; +/// list[index] = value; +/// old_value +/// }; +/// +/// // Since this closure mutably borrows data, we can't convert it into a regular `DynamicFunction`, +/// // as doing so would result in a compile-time error: +/// // let mut func: DynamicFunction = replace.into_function(); +/// +/// // Instead, we convert it into a `DynamicFunctionMut` using `IntoFunctionMut::into_function_mut`: +/// let mut func: DynamicFunctionMut = replace.into_function_mut(); +/// +/// // Dynamically call it: +/// let args = ArgList::default().push_owned(1_usize).push_owned(-2_i32); +/// let value = func.call(args).unwrap().unwrap_owned(); +/// +/// // Check the result: +/// assert_eq!(value.try_take::().unwrap(), 2); +/// +/// // Note that `func` still has a reference to `list`, +/// // so we need to drop it before we can access `list` again. +/// // Alternatively, we could have invoked `func` with +/// // `DynamicFunctionMut::call_once` to immediately consume it. +/// drop(func); +/// assert_eq!(list, vec![1, -2, 3]); +/// ``` +/// +/// [`ReflectFnMut`]: crate::func::ReflectFnMut +/// [module-level documentation]: crate::func +pub struct DynamicFunctionMut<'env> { + info: FunctionInfo, + func: Box FnMut(ArgList<'a>) -> FunctionResult<'a> + 'env>, +} + +impl<'env> DynamicFunctionMut<'env> { + /// Create a new [`DynamicFunctionMut`]. + /// + /// The given function can be used to call out to any other callable, + /// including functions, closures, or methods. + /// + /// It's important that the function signature matches the provided [`FunctionInfo`]. + /// This info may be used by consumers of this function for validation and debugging. + pub fn new FnMut(ArgList<'a>) -> FunctionResult<'a> + 'env>( + func: F, + info: FunctionInfo, + ) -> Self { + Self { + info, + func: Box::new(func), + } + } + + /// Set the name of the function. + /// + /// For [`DynamicFunctionMuts`] created using [`IntoFunctionMut`], + /// the default name will always be the full path to the function as returned by [`std::any::type_name`], + /// unless the function is a closure, anonymous function, or function pointer, + /// in which case the name will be `None`. + /// + /// [`DynamicFunctionMuts`]: DynamicFunctionMut + pub fn with_name(mut self, name: impl Into>) -> Self { + self.info = self.info.with_name(name); + self + } + + /// Set the argument information of the function. + /// + /// It's important that the arguments match the intended function signature, + /// as this can be used by consumers of this function for validation and debugging. + pub fn with_args(mut self, args: Vec) -> Self { + self.info = self.info.with_args(args); + self + } + + /// Set the return information of the function. + pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self { + self.info = self.info.with_return_info(return_info); + self + } + + /// Call the function with the given arguments. + /// + /// Variables that are captured mutably by this function + /// won't be usable until this function is dropped. + /// Consider using [`call_once`] if you want to consume the function + /// immediately after calling it. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::{IntoFunctionMut, ArgList}; + /// let mut total = 0; + /// let add = |a: i32, b: i32| -> i32 { + /// total = a + b; + /// total + /// }; + /// + /// let mut func = add.into_function_mut().with_name("add"); + /// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + /// let result = func.call(args).unwrap().unwrap_owned(); + /// assert_eq!(result.try_take::().unwrap(), 100); + /// ``` + /// + /// [`call_once`]: DynamicFunctionMut::call_once + pub fn call<'a>(&mut self, args: ArgList<'a>) -> FunctionResult<'a> { + (self.func)(args) + } + + /// Call the function with the given arguments and consume it. + /// + /// This is useful for functions that capture their environment mutably + /// because otherwise any captured variables would still be borrowed by it. + /// + /// # Example + /// + /// ``` + /// # use bevy_reflect::func::{IntoFunctionMut, ArgList}; + /// let mut count = 0; + /// let increment = |amount: i32| count += amount; + /// + /// let increment_function = increment.into_function_mut(); + /// let args = ArgList::new().push_owned(5_i32); + /// + /// // We need to drop `increment_function` here so that we + /// // can regain access to `count`. + /// // `call_once` does this automatically for us. + /// increment_function.call_once(args).unwrap(); + /// assert_eq!(count, 5); + /// ``` + pub fn call_once(mut self, args: ArgList) -> FunctionResult { + (self.func)(args) + } + + /// Returns the function info. + pub fn info(&self) -> &FunctionInfo { + &self.info + } + + /// The [name] of the function. + /// + /// For [`DynamicFunctionMuts`] created using [`IntoFunctionMut`], + /// the default name will always be the full path to the function as returned by [`std::any::type_name`], + /// unless the function is a closure, anonymous function, or function pointer, + /// in which case the name will be `None`. + /// + /// This can be overridden using [`with_name`]. + /// + /// [name]: FunctionInfo::name + /// [`DynamicFunctionMuts`]: DynamicFunctionMut + /// [`with_name`]: Self::with_name + pub fn name(&self) -> Option<&Cow<'static, str>> { + self.info.name() + } +} + +/// Outputs the function's signature. +/// +/// This takes the format: `DynamicFunctionMut(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`. +/// +/// Names for arguments and the function itself are optional and will default to `_` if not provided. +impl<'env> Debug for DynamicFunctionMut<'env> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + let name = self.info.name().unwrap_or(&Cow::Borrowed("_")); + write!(f, "DynamicFunctionMut(fn {name}(")?; + + for (index, arg) in self.info.args().iter().enumerate() { + let name = arg.name().unwrap_or("_"); + let ty = arg.type_path(); + write!(f, "{name}: {ty}")?; + + if index + 1 < self.info.args().len() { + write!(f, ", ")?; + } + } + + let ret = self.info.return_info().type_path(); + write!(f, ") -> {ret})") + } +} + +impl<'env> From> for DynamicFunctionMut<'env> { + #[inline] + fn from(function: DynamicFunction<'env>) -> Self { + Self { + info: function.info, + func: Box::new(move |args| (function.func)(args)), + } + } +} + +impl<'env> IntoFunctionMut<'env, ()> for DynamicFunctionMut<'env> { + #[inline] + fn into_function_mut(self) -> DynamicFunctionMut<'env> { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_overwrite_function_name() { + let mut total = 0; + let func = (|a: i32, b: i32| total = a + b) + .into_function_mut() + .with_name("my_function"); + assert_eq!(func.info().name().unwrap(), "my_function"); + } + + #[test] + fn should_convert_dynamic_function_mut_with_into_function() { + fn make_closure<'env, F: IntoFunctionMut<'env, M>, M>(f: F) -> DynamicFunctionMut<'env> { + f.into_function_mut() + } + + let mut total = 0; + let closure: DynamicFunctionMut = make_closure(|a: i32, b: i32| total = a + b); + let _: DynamicFunctionMut = make_closure(closure); + } +} diff --git a/crates/bevy_reflect/src/func/error.rs b/crates/bevy_reflect/src/func/error.rs index 65290b66d4b59a..a37123f1b6cbab 100644 --- a/crates/bevy_reflect/src/func/error.rs +++ b/crates/bevy_reflect/src/func/error.rs @@ -1,9 +1,12 @@ use crate::func::args::ArgError; +use crate::func::Return; +use alloc::borrow::Cow; use thiserror::Error; -/// An error that occurs when calling a [`DynamicFunction`]. +/// An error that occurs when calling a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Error, PartialEq)] pub enum FunctionError { /// An error occurred while converting an argument. @@ -11,5 +14,29 @@ pub enum FunctionError { ArgError(#[from] ArgError), /// The number of arguments provided does not match the expected number. #[error("expected {expected} arguments but received {received}")] - InvalidArgCount { expected: usize, received: usize }, + ArgCountMismatch { expected: usize, received: usize }, +} + +/// The result of calling a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// Returns `Ok(value)` if the function was called successfully, +/// where `value` is the [`Return`] value of the function. +/// +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut +pub type FunctionResult<'a> = Result, FunctionError>; + +/// An error that occurs when registering a function into a [`FunctionRegistry`]. +/// +/// [`FunctionRegistry`]: crate::func::FunctionRegistry +#[derive(Debug, Error, PartialEq)] +pub enum FunctionRegistrationError { + /// A function with the given name has already been registered. + /// + /// Contains the duplicate function name. + #[error("a function has already been registered with name {0:?}")] + DuplicateName(Cow<'static, str>), + /// The function is missing a name by which it can be registered. + #[error("function name is missing")] + MissingName, } diff --git a/crates/bevy_reflect/src/func/function.rs b/crates/bevy_reflect/src/func/function.rs deleted file mode 100644 index 6ac4b1bbf8d857..00000000000000 --- a/crates/bevy_reflect/src/func/function.rs +++ /dev/null @@ -1,220 +0,0 @@ -use crate::func::args::{ArgInfo, ArgList}; -use crate::func::error::FunctionError; -use crate::func::info::FunctionInfo; -use crate::func::return_type::Return; -use crate::func::{IntoFunction, ReturnInfo}; -use alloc::borrow::Cow; -use core::fmt::{Debug, Formatter}; -use std::ops::DerefMut; - -/// The result of calling a dynamic [`DynamicFunction`]. -/// -/// Returns `Ok(value)` if the function was called successfully, -/// where `value` is the [`Return`] value of the function. -pub type FunctionResult<'a> = Result, FunctionError>; - -/// A dynamic representation of a Rust function. -/// -/// Internally this stores a function pointer and associated info. -/// -/// You will generally not need to construct this manually. -/// Instead, many functions and closures can be automatically converted using the [`IntoFunction`] trait. -/// -/// # Example -/// -/// Most of the time, a [`DynamicFunction`] can be created using the [`IntoFunction`] trait: -/// -/// ``` -/// # use bevy_reflect::func::args::ArgList; -/// # use bevy_reflect::func::{DynamicFunction, IntoFunction}; -/// fn add(a: i32, b: i32) -> i32 { -/// a + b -/// } -/// -/// // Convert the function into a dynamic function using `IntoFunction::into_function` -/// let mut func: DynamicFunction = add.into_function(); -/// -/// // Dynamically call the function: -/// let args = ArgList::default().push_owned(25_i32).push_owned(75_i32); -/// let value = func.call(args).unwrap().unwrap_owned(); -/// -/// // Check the result: -/// assert_eq!(value.downcast_ref::(), Some(&100)); -/// ``` -/// -/// However, in some cases, these functions may need to be created manually: -/// -/// ``` -/// # use bevy_reflect::func::{ArgList, DynamicFunction, FunctionInfo, IntoFunction, Return, ReturnInfo}; -/// # use bevy_reflect::func::args::ArgInfo; -/// fn append(value: String, list: &mut Vec) -> &mut String { -/// list.push(value); -/// list.last_mut().unwrap() -/// } -/// -/// // Due to the return value being a reference that is not tied to the first argument, -/// // this will fail to compile: -/// // let mut func: DynamicFunction = append.into_function(); -/// -/// // Instead, we need to define the function manually. -/// // We start by defining the shape of the function: -/// let info = FunctionInfo::new() -/// .with_name("append") -/// .with_args(vec![ -/// ArgInfo::new::(0).with_name("value"), -/// ArgInfo::new::<&mut Vec>(1).with_name("list"), -/// ]) -/// .with_return_info( -/// ReturnInfo::new::<&mut String>() -/// ); -/// -/// // Then we define the dynamic function, which will be used to call our `append` function: -/// let mut func = DynamicFunction::new(|mut args, info| { -/// // Arguments are popped from the list in reverse order: -/// let arg1 = args.pop().unwrap().take_mut::>(&info.args()[1]).unwrap(); -/// let arg0 = args.pop().unwrap().take_owned::(&info.args()[0]).unwrap(); -/// -/// // Then we can call our function and return the result: -/// Ok(Return::Mut(append(arg0, arg1))) -/// }, info); -/// -/// let mut list = Vec::::new(); -/// -/// // Dynamically call the function: -/// let args = ArgList::default().push_owned("Hello, World".to_string()).push_mut(&mut list); -/// let value = func.call(args).unwrap().unwrap_mut(); -/// -/// // Mutate the return value: -/// value.downcast_mut::().unwrap().push_str("!!!"); -/// -/// // Check the result: -/// assert_eq!(list, vec!["Hello, World!!!"]); -/// ``` -pub struct DynamicFunction<'env> { - info: FunctionInfo, - func: Box FnMut(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>, -} - -impl<'env> DynamicFunction<'env> { - /// Create a new dynamic [`DynamicFunction`]. - /// - /// The given function can be used to call out to a regular function, closure, or method. - /// - /// It's important that the function signature matches the provided [`FunctionInfo`]. - /// This info is used to validate the arguments and return value. - pub fn new FnMut(ArgList<'a>, &FunctionInfo) -> FunctionResult<'a> + 'env>( - func: F, - info: FunctionInfo, - ) -> Self { - Self { - info, - func: Box::new(func), - } - } - - /// Set the name of the function. - /// - /// For [`DynamicFunctions`] created using [`IntoFunction`], - /// the default name will always be the full path to the function as returned by [`std::any::type_name`]. - /// - /// [`DynamicFunctions`]: DynamicFunction - pub fn with_name(mut self, name: impl Into>) -> Self { - self.info = self.info.with_name(name); - self - } - - /// Set the arguments of the function. - /// - /// It is very important that the arguments match the intended function signature, - /// as this is used to validate arguments passed to the function. - pub fn with_args(mut self, args: Vec) -> Self { - self.info = self.info.with_args(args); - self - } - - /// Set the return information of the function. - pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self { - self.info = self.info.with_return_info(return_info); - self - } - - /// Call the function with the given arguments. - /// - /// # Example - /// - /// ``` - /// # use bevy_reflect::func::{IntoFunction, ArgList}; - /// fn add(left: i32, right: i32) -> i32 { - /// left + right - /// } - /// - /// let mut func = add.into_function(); - /// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); - /// let result = func.call(args).unwrap().unwrap_owned(); - /// assert_eq!(result.take::().unwrap(), 100); - /// ``` - pub fn call<'a>(&mut self, args: ArgList<'a>) -> FunctionResult<'a> { - (self.func.deref_mut())(args, &self.info) - } - - /// Call the function with the given arguments and consume the function. - /// - /// This is useful for closures that capture their environment because otherwise - /// any captured variables would still be borrowed by this function. - /// - /// # Example - /// - /// ``` - /// # use bevy_reflect::func::{IntoFunction, ArgList}; - /// let mut count = 0; - /// let increment = |amount: i32| { - /// count += amount; - /// }; - /// let increment_function = increment.into_function(); - /// let args = ArgList::new().push_owned(5_i32); - /// // We need to drop `increment_function` here so that we - /// // can regain access to `count`. - /// increment_function.call_once(args).unwrap(); - /// assert_eq!(count, 5); - /// ``` - pub fn call_once(mut self, args: ArgList) -> FunctionResult { - (self.func.deref_mut())(args, &self.info) - } - - /// Returns the function info. - pub fn info(&self) -> &FunctionInfo { - &self.info - } -} - -/// Outputs the function signature. -/// -/// This takes the format: `DynamicFunction(fn {name}({arg1}: {type1}, {arg2}: {type2}, ...) -> {return_type})`. -/// -/// Names for arguments and the function itself are optional and will default to `_` if not provided. -impl<'env> Debug for DynamicFunction<'env> { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - let name = self.info.name().unwrap_or("_"); - write!(f, "DynamicFunction(fn {name}(")?; - - for (index, arg) in self.info.args().iter().enumerate() { - let name = arg.name().unwrap_or("_"); - let ty = arg.type_path(); - write!(f, "{name}: {ty}")?; - - if index + 1 < self.info.args().len() { - write!(f, ", ")?; - } - } - - let ret = self.info.return_info().type_path(); - write!(f, ") -> {ret})") - } -} - -impl<'env> IntoFunction<'env, ()> for DynamicFunction<'env> { - #[inline] - fn into_function(self) -> DynamicFunction<'env> { - self - } -} diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 49c0789c585516..413af8a3ed5110 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -1,10 +1,17 @@ +use alloc::borrow::Cow; + +use bevy_utils::all_tuples; + use crate::func::args::{ArgInfo, GetOwnership, Ownership}; use crate::TypePath; -use alloc::borrow::Cow; -/// Type information for a [`DynamicFunction`]. +/// Type information for a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// This information can be retrieved directly from certain functions and closures +/// using the [`TypedFunction`] trait, and manually constructed otherwise. /// /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct FunctionInfo { name: Option>, @@ -13,10 +20,22 @@ pub struct FunctionInfo { } impl FunctionInfo { - /// Create a new [`FunctionInfo`]. + /// Create a new [`FunctionInfo`] for a function with the given name. + pub fn named(name: impl Into>) -> Self { + Self { + name: Some(name.into()), + args: Vec::new(), + return_info: ReturnInfo::new::<()>(), + } + } + + /// Create a new [`FunctionInfo`] with no name. /// - /// To set the name of the function, use [`Self::with_name`]. - pub fn new() -> Self { + /// For the purposes of debugging and [registration], + /// it's recommended to use [`FunctionInfo::named`] instead. + /// + /// [registration]: crate::func::FunctionRegistry + pub fn anonymous() -> Self { Self { name: None, args: Vec::new(), @@ -24,41 +43,79 @@ impl FunctionInfo { } } + /// Create a new [`FunctionInfo`] from the given function. + pub fn from(function: &F) -> Self + where + F: TypedFunction, + { + function.get_function_info() + } + /// Set the name of the function. - /// - /// Reflected functions are not required to have a name, - /// so this method must be called manually to set the name. pub fn with_name(mut self, name: impl Into>) -> Self { self.name = Some(name.into()); self } + /// Push an argument onto the function's argument list. + /// + /// The order in which this method is called matters as it will determine the index of the argument + /// based on the current number of arguments. + pub fn with_arg( + mut self, + name: impl Into>, + ) -> Self { + let index = self.args.len(); + self.args.push(ArgInfo::new::(index).with_name(name)); + self + } + /// Set the arguments of the function. /// - /// Arguments passed to the function will be validated against the info provided here. - /// Mismatched arguments may result in the function call returning an [error]. + /// This will completely replace any existing arguments. /// - /// [error]: crate::func::FunctionError + /// It's preferable to use [`Self::with_arg`] to add arguments to the function + /// as it will automatically set the index of the argument. pub fn with_args(mut self, args: Vec) -> Self { self.args = args; self } - /// Set the return information of the function. + /// Set the [return information] of the function. + /// + /// To manually set the [`ReturnInfo`] of the function, see [`Self::with_return_info`]. + /// + /// [return information]: ReturnInfo + pub fn with_return(mut self) -> Self { + self.return_info = ReturnInfo::new::(); + self + } + + /// Set the [return information] of the function. + /// + /// This will completely replace any existing return information. + /// + /// For a simpler, static version of this method, see [`Self::with_return`]. + /// + /// [return information]: ReturnInfo pub fn with_return_info(mut self, return_info: ReturnInfo) -> Self { self.return_info = return_info; self } - /// The name of the function, if it was given one. + /// The name of the function. /// - /// For [`DynamicFunctions`] created using [`IntoFunction`], - /// the name will always be the full path to the function as returned by [`std::any::type_name`]. + /// For [`DynamicFunctions`] created using [`IntoFunction`] or [`DynamicFunctionMuts`] created using [`IntoFunctionMut`], + /// the default name will always be the full path to the function as returned by [`std::any::type_name`], + /// unless the function is a closure, anonymous function, or function pointer, + /// in which case the name will be `None`. /// /// [`DynamicFunctions`]: crate::func::DynamicFunction /// [`IntoFunction`]: crate::func::IntoFunction - pub fn name(&self) -> Option<&str> { - self.name.as_deref() + /// [`DynamicFunctionMuts`]: crate::func::DynamicFunctionMut + /// [`IntoFunctionMut`]: crate::func::IntoFunctionMut + pub fn name(&self) -> Option<&Cow<'static, str>> { + self.name.as_ref() } /// The arguments of the function. @@ -77,15 +134,10 @@ impl FunctionInfo { } } -impl Default for FunctionInfo { - fn default() -> Self { - Self::new() - } -} - -/// Information about the return type of a [`DynamicFunction`]. +/// Information about the return type of a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct ReturnInfo { type_path: &'static str, @@ -111,3 +163,285 @@ impl ReturnInfo { self.ownership } } + +/// A static accessor to compile-time type information for functions. +/// +/// This is the equivalent of [`Typed`], but for function. +/// +/// # Blanket Implementation +/// +/// This trait has a blanket implementation that covers: +/// - Functions and methods defined with the `fn` keyword +/// - Anonymous functions +/// - Function pointers +/// - Closures that capture immutable references to their environment +/// - Closures that capture mutable references to their environment +/// - Closures that take ownership of captured variables +/// +/// For each of the above cases, the function signature may only have up to 15 arguments, +/// not including an optional receiver argument (often `&self` or `&mut self`). +/// This optional receiver argument may be either a mutable or immutable reference to a type. +/// If the return type is also a reference, its lifetime will be bound to the lifetime of this receiver. +/// +/// See the [module-level documentation] for more information on valid signatures. +/// +/// Arguments and the return type are expected to implement both [`GetOwnership`] and [`TypePath`]. +/// By default, these traits are automatically implemented when using the `Reflect` [derive macro]. +/// +/// # Example +/// +/// ``` +/// # use bevy_reflect::func::{ArgList, FunctionInfo, ReflectFnMut, TypedFunction}; +/// # +/// fn print(value: String) { +/// println!("{}", value); +/// } +/// +/// let info = print.get_function_info(); +/// assert!(info.name().unwrap().ends_with("print")); +/// assert_eq!(info.arg_count(), 1); +/// assert_eq!(info.args()[0].type_path(), "alloc::string::String"); +/// assert_eq!(info.return_info().type_path(), "()"); +/// ``` +/// +/// # Trait Parameters +/// +/// This trait has a `Marker` type parameter that is used to get around issues with +/// [unconstrained type parameters] when defining impls with generic arguments or return types. +/// This `Marker` can be any type, provided it doesn't conflict with other implementations. +/// +/// [module-level documentation]: crate::func +/// [`Typed`]: crate::Typed +/// [unconstrained type parameters]: https://doc.rust-lang.org/error_codes/E0207.html +pub trait TypedFunction { + /// Get the [`FunctionInfo`] for this type. + fn function_info() -> FunctionInfo; + + /// Get the [`FunctionInfo`] for this type. + fn get_function_info(&self) -> FunctionInfo { + Self::function_info() + } +} + +/// Helper macro for implementing [`TypedFunction`] on Rust functions. +/// +/// This currently implements it for the following signatures (where `argX` may be any of `T`, `&T`, or `&mut T`): +/// - `FnMut(arg0, arg1, ..., argN) -> R` +/// - `FnMut(&Receiver, arg0, arg1, ..., argN) -> &R` +/// - `FnMut(&mut Receiver, arg0, arg1, ..., argN) -> &mut R` +/// - `FnMut(&mut Receiver, arg0, arg1, ..., argN) -> &R` +macro_rules! impl_typed_function { + ($(($Arg:ident, $arg:ident)),*) => { + // === (...) -> ReturnType === // + impl<$($Arg,)* ReturnType, Function> TypedFunction [ReturnType]> for Function + where + $($Arg: TypePath + GetOwnership,)* + ReturnType: TypePath + GetOwnership, + Function: FnMut($($Arg),*) -> ReturnType, + { + fn function_info() -> FunctionInfo { + create_info::() + .with_args({ + #[allow(unused_mut)] + let mut _index = 0; + vec![ + $(ArgInfo::new::<$Arg>({ + _index += 1; + _index - 1 + }),)* + ] + }) + .with_return_info(ReturnInfo::new::()) + } + } + + // === (&self, ...) -> &ReturnType === // + impl TypedFunction &ReturnType> for Function + where + for<'a> &'a Receiver: TypePath + GetOwnership, + $($Arg: TypePath + GetOwnership,)* + for<'a> &'a ReturnType: TypePath + GetOwnership, + Function: for<'a> FnMut(&'a Receiver, $($Arg),*) -> &'a ReturnType, + { + fn function_info() -> $crate::func::FunctionInfo { + create_info::() + .with_args({ + #[allow(unused_mut)] + let mut _index = 1; + vec![ + ArgInfo::new::<&Receiver>(0), + $($crate::func::args::ArgInfo::new::<$Arg>({ + _index += 1; + _index - 1 + }),)* + ] + }) + .with_return_info(ReturnInfo::new::<&ReturnType>()) + } + } + + // === (&mut self, ...) -> &mut ReturnType === // + impl TypedFunction &mut ReturnType> for Function + where + for<'a> &'a mut Receiver: TypePath + GetOwnership, + $($Arg: TypePath + GetOwnership,)* + for<'a> &'a mut ReturnType: TypePath + GetOwnership, + Function: for<'a> FnMut(&'a mut Receiver, $($Arg),*) -> &'a mut ReturnType, + { + fn function_info() -> FunctionInfo { + create_info::() + .with_args({ + #[allow(unused_mut)] + let mut _index = 1; + vec![ + ArgInfo::new::<&mut Receiver>(0), + $(ArgInfo::new::<$Arg>({ + _index += 1; + _index - 1 + }),)* + ] + }) + .with_return_info(ReturnInfo::new::<&mut ReturnType>()) + } + } + + // === (&mut self, ...) -> &ReturnType === // + impl TypedFunction &ReturnType> for Function + where + for<'a> &'a mut Receiver: TypePath + GetOwnership, + $($Arg: TypePath + GetOwnership,)* + for<'a> &'a ReturnType: TypePath + GetOwnership, + Function: for<'a> FnMut(&'a mut Receiver, $($Arg),*) -> &'a ReturnType, + { + fn function_info() -> FunctionInfo { + create_info::() + .with_args({ + #[allow(unused_mut)] + let mut _index = 1; + vec![ + ArgInfo::new::<&mut Receiver>(0), + $(ArgInfo::new::<$Arg>({ + _index += 1; + _index - 1 + }),)* + ] + }) + .with_return_info(ReturnInfo::new::<&ReturnType>()) + } + } + }; +} + +all_tuples!(impl_typed_function, 0, 15, Arg, arg); + +/// Helper function for creating [`FunctionInfo`] with the proper name value. +/// +/// Names are only given if: +/// - The function is not a closure +/// - The function is not a function pointer +/// - The function is not an anonymous function +/// +/// This function relies on the [`type_name`] of `F` to determine this. +/// The following table describes the behavior for different types of functions: +/// +/// | Category | `type_name` | `FunctionInfo::name` | +/// | ------------------ | ----------------------- | ----------------------- | +/// | Named function | `foo::bar::baz` | `Some("foo::bar::baz")` | +/// | Closure | `foo::bar::{{closure}}` | `None` | +/// | Anonymous function | `foo::bar::{{closure}}` | `None` | +/// | Function pointer | `fn() -> String` | `None` | +/// +/// [`type_name`]: std::any::type_name +fn create_info() -> FunctionInfo { + let name = std::any::type_name::(); + + if name.ends_with("{{closure}}") || name.starts_with("fn(") { + FunctionInfo::anonymous() + } else { + FunctionInfo::named(name) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_create_function_info() { + fn add(a: i32, b: i32) -> i32 { + a + b + } + + // Sanity check: + assert_eq!( + std::any::type_name_of_val(&add), + "bevy_reflect::func::info::tests::should_create_function_info::add" + ); + + let info = add.get_function_info(); + assert_eq!( + info.name().unwrap(), + "bevy_reflect::func::info::tests::should_create_function_info::add" + ); + assert_eq!(info.arg_count(), 2); + assert_eq!(info.args()[0].type_path(), "i32"); + assert_eq!(info.args()[1].type_path(), "i32"); + assert_eq!(info.return_info().type_path(), "i32"); + } + + #[test] + fn should_create_function_pointer_info() { + fn add(a: i32, b: i32) -> i32 { + a + b + } + + let add = add as fn(i32, i32) -> i32; + + // Sanity check: + assert_eq!(std::any::type_name_of_val(&add), "fn(i32, i32) -> i32"); + + let info = add.get_function_info(); + assert!(info.name().is_none()); + assert_eq!(info.arg_count(), 2); + assert_eq!(info.args()[0].type_path(), "i32"); + assert_eq!(info.args()[1].type_path(), "i32"); + assert_eq!(info.return_info().type_path(), "i32"); + } + + #[test] + fn should_create_anonymous_function_info() { + let add = |a: i32, b: i32| a + b; + + // Sanity check: + assert_eq!( + std::any::type_name_of_val(&add), + "bevy_reflect::func::info::tests::should_create_anonymous_function_info::{{closure}}" + ); + + let info = add.get_function_info(); + assert!(info.name().is_none()); + assert_eq!(info.arg_count(), 2); + assert_eq!(info.args()[0].type_path(), "i32"); + assert_eq!(info.args()[1].type_path(), "i32"); + assert_eq!(info.return_info().type_path(), "i32"); + } + + #[test] + fn should_create_closure_info() { + let mut total = 0; + let add = |a: i32, b: i32| total = a + b; + + // Sanity check: + assert_eq!( + std::any::type_name_of_val(&add), + "bevy_reflect::func::info::tests::should_create_closure_info::{{closure}}" + ); + + let info = add.get_function_info(); + assert!(info.name().is_none()); + assert_eq!(info.arg_count(), 2); + assert_eq!(info.args()[0].type_path(), "i32"); + assert_eq!(info.args()[1].type_path(), "i32"); + assert_eq!(info.return_info().type_path(), "()"); + } +} diff --git a/crates/bevy_reflect/src/func/into_function.rs b/crates/bevy_reflect/src/func/into_function.rs index 30609c948ccfd8..3b52605aa31f45 100644 --- a/crates/bevy_reflect/src/func/into_function.rs +++ b/crates/bevy_reflect/src/func/into_function.rs @@ -1,318 +1,68 @@ -use crate::func::function::DynamicFunction; -use bevy_utils::all_tuples; +use crate::func::{DynamicFunction, ReflectFn, TypedFunction}; /// A trait for types that can be converted into a [`DynamicFunction`]. /// -/// # Blanket Implementation +/// This trait is automatically implemented for any type that implements +/// [`ReflectFn`] and [`TypedFunction`]. /// -/// This trait has a blanket implementation that covers many functions, closures, and methods. -/// And though it works for many cases, it does have some limitations. +/// See the [module-level documentation] for more information. /// -/// ## Arguments +/// # Trait Parameters /// -/// Firstly, the function signature may only have up to 15 arguments -/// (or 16 if the first argument is a mutable/immutable reference). -/// This limitation is unfortunately due to the [lack of variadic generics] in Rust. +/// This trait has a `Marker` type parameter that is used to get around issues with +/// [unconstrained type parameters] when defining impls with generic arguments or return types. +/// This `Marker` can be any type, provided it doesn't conflict with other implementations. /// -/// Each argument must implement [`FromArg`], [`GetOwnership`], and [`TypePath`]. +/// Additionally, it has a lifetime parameter, `'env`, that is used to bound the lifetime of the function. +/// For named functions and some closures, this will end up just being `'static`, +/// however, closures that borrow from their environment will have a lifetime bound to that environment. /// -/// -/// ```compile_fail -/// # use bevy_reflect::func::IntoFunction; -/// fn too_many_args( -/// arg01: i32, -/// arg02: i32, -/// arg03: i32, -/// arg04: i32, -/// arg05: i32, -/// arg06: i32, -/// arg07: i32, -/// arg08: i32, -/// arg09: i32, -/// arg10: i32, -/// arg11: i32, -/// arg12: i32, -/// arg13: i32, -/// arg14: i32, -/// arg15: i32, -/// arg16: i32, -/// ) { -/// // ... -/// } -/// -/// // This will fail to compile: -/// too_many_args.into_function(); -/// ``` -/// -/// ## Return Type -/// -/// Secondly, the allowed return type is dependent on the first argument of the function: -/// - If the first argument is an immutable reference, -/// then the return type may be either an owned type, a static reference type, or a reference type -/// bound to the lifetime of the first argument. -/// - If the first argument is a mutable reference, -/// then the return type may be either an owned type, a static reference type, or be a mutable reference type -/// bound to the lifetime of the first argument. -/// - If the first argument is an owned type, -/// then the return type may be either an owned type or a static reference type. -/// -/// The return type must always implement [`GetOwnership`] and [`TypePath`]. -/// If it is either an owned type or a static reference type, -/// then it must also implement [`IntoReturn`]. -/// Otherwise, it must also implement [`Reflect`]. -/// -/// Note that both `GetOwnership`, `TypePath`, and `IntoReturn` are automatically implemented -/// when [deriving `Reflect`]. -/// -/// ``` -/// # use bevy_reflect::func::IntoFunction; -/// fn owned_return(arg: i32) -> i32 { arg * 2 } -/// fn ref_return(arg: &i32) -> &i32 { arg } -/// fn mut_return(arg: &mut i32) -> &mut i32 { arg } -/// fn static_return(arg: i32) -> &'static i32 { &123 } -/// -/// owned_return.into_function(); -/// ref_return.into_function(); -/// mut_return.into_function(); -/// static_return.into_function(); -/// ``` -/// -/// [lack of variadic generics]: https://poignardazur.github.io/2024/05/25/report-on-rustnl-variadics/ -/// [`FromArg`]: crate::func::args::FromArg -/// [`GetOwnership`]: crate::func::args::GetOwnership -/// [`TypePath`]: crate::TypePath -/// [`IntoReturn`]: crate::func::IntoReturn -/// [`Reflect`]: crate::Reflect -/// [deriving `Reflect`]: derive@crate::Reflect +/// [module-level documentation]: crate::func +/// [unconstrained type parameters]: https://doc.rust-lang.org/error_codes/E0207.html pub trait IntoFunction<'env, Marker> { /// Converts [`Self`] into a [`DynamicFunction`]. fn into_function(self) -> DynamicFunction<'env>; } -/// Helper macro that returns the number of tokens it receives. -/// -/// This is used to get the argument count. -/// -/// See [here] for details. -/// -/// [here]: https://veykril.github.io/tlborm/decl-macros/building-blocks/counting.html#bit-twiddling -macro_rules! count_tts { - () => { 0 }; - ($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 }; - ($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 }; +impl<'env, F, Marker1, Marker2> IntoFunction<'env, (Marker1, Marker2)> for F +where + F: ReflectFn<'env, Marker1> + TypedFunction + Send + Sync + 'env, +{ + fn into_function(self) -> DynamicFunction<'env> { + DynamicFunction::new(move |args| self.reflect_call(args), Self::function_info()) + } } -/// Helper macro for implementing [`IntoFunction`] on Rust functions. -/// -/// This currently implements it for the following signatures (where `argX` may be any of `T`, `&T`, or `&mut T`): -/// - `fn(arg0, arg1, ..., argN) -> R` -/// - `fn(&Receiver, arg0, arg1, ..., argN) -> &R` -/// - `fn(&mut Receiver, arg0, arg1, ..., argN) -> &mut R` -/// - `fn(&mut Receiver, arg0, arg1, ..., argN) -> &R` -macro_rules! impl_into_function { - ($(($Arg:ident, $arg:ident)),*) => { - // === Owned Return === // - impl<'env, $($Arg,)* R, F> $crate::func::IntoFunction<'env, fn($($Arg),*) -> R> for F - where - $($Arg: $crate::func::args::FromArg + $crate::func::args::GetOwnership + $crate::TypePath,)* - R: $crate::func::IntoReturn + $crate::func::args::GetOwnership + $crate::TypePath, - F: FnMut($($Arg),*) -> R + 'env, - F: for<'a> FnMut($($Arg::Item<'a>),*) -> R + 'env, - { - fn into_function(mut self) -> $crate::func::DynamicFunction<'env> { - const COUNT: usize = count_tts!($($Arg)*); - - let info = $crate::func::FunctionInfo::new() - .with_name(std::any::type_name::()) - .with_args({ - #[allow(unused_mut)] - let mut _index = 0; - vec![ - $($crate::func::args::ArgInfo::new::<$Arg>({ - _index += 1; - _index - 1 - }),)* - ] - }) - .with_return_info($crate::func::ReturnInfo::new::()); - - $crate::func::DynamicFunction::new(move |args, _info| { - if args.len() != COUNT { - return Err($crate::func::error::FunctionError::InvalidArgCount { - expected: COUNT, - received: args.len(), - }); - } - - let [$($arg,)*] = args.take().try_into().expect("invalid number of arguments"); - - #[allow(unused_mut)] - let mut _index = 0; - let ($($arg,)*) = ($($Arg::from_arg($arg, { - _index += 1; - _info.args().get(_index - 1).expect("argument index out of bounds") - })?,)*); - Ok((self)($($arg,)*).into_return()) - }, info) - } - } - - // === Ref Receiver + Ref Return === // - impl<'env, Receiver, $($Arg,)* R, F> $crate::func::IntoFunction<'env, fn(&Receiver, $($Arg),*) -> fn(&R)> for F - where - Receiver: $crate::Reflect + $crate::TypePath, - for<'a> &'a Receiver: $crate::func::args::GetOwnership, - R: $crate::Reflect + $crate::TypePath, - for<'a> &'a R: $crate::func::args::GetOwnership, - $($Arg: $crate::func::args::FromArg + $crate::func::args::GetOwnership + $crate::TypePath,)* - F: for<'a> FnMut(&'a Receiver, $($Arg),*) -> &'a R + 'env, - F: for<'a> FnMut(&'a Receiver, $($Arg::Item<'a>),*) -> &'a R + 'env, - { - fn into_function(mut self) -> $crate::func::DynamicFunction<'env> { - const COUNT: usize = count_tts!(Receiver $($Arg)*); - - let info = $crate::func::FunctionInfo::new() - .with_name(std::any::type_name::()) - .with_args({ - #[allow(unused_mut)] - let mut _index = 1; - vec![ - $crate::func::args::ArgInfo::new::<&Receiver>(0), - $($crate::func::args::ArgInfo::new::<$Arg>({ - _index += 1; - _index - 1 - }),)* - ] - }) - .with_return_info($crate::func::ReturnInfo::new::<&R>()); - - $crate::func::DynamicFunction::new(move |args, _info| { - if args.len() != COUNT { - return Err($crate::func::error::FunctionError::InvalidArgCount { - expected: COUNT, - received: args.len(), - }); - } - - let [receiver, $($arg,)*] = args.take().try_into().expect("invalid number of arguments"); - - let receiver = receiver.take_ref::(_info.args().get(0).expect("argument index out of bounds"))?; - - #[allow(unused_mut)] - let mut _index = 1; - let ($($arg,)*) = ($($Arg::from_arg($arg, { - _index += 1; - _info.args().get(_index - 1).expect("argument index out of bounds") - })?,)*); - Ok($crate::func::Return::Ref((self)(receiver, $($arg,)*))) - }, info) - } +#[cfg(test)] +mod tests { + use super::*; + use crate::func::ArgList; + + #[test] + fn should_create_dynamic_function_from_closure() { + let c = 23; + let func = (|a: i32, b: i32| a + b + c).into_function(); + let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + let result = func.call(args).unwrap().unwrap_owned(); + assert_eq!(result.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_create_dynamic_function_from_function() { + fn add(a: i32, b: i32) -> i32 { + a + b } - // === Mut Receiver + Mut Return === // - impl<'env, Receiver, $($Arg,)* R, F> $crate::func::IntoFunction<'env, fn(&mut Receiver, $($Arg),*) -> fn(&mut R)> for F - where - Receiver: $crate::Reflect + $crate::TypePath, - for<'a> &'a mut Receiver: $crate::func::args::GetOwnership, - R: $crate::Reflect + $crate::TypePath, - for<'a> &'a mut R: $crate::func::args::GetOwnership, - $($Arg: $crate::func::args::FromArg + $crate::func::args::GetOwnership + $crate::TypePath,)* - F: for<'a> FnMut(&'a mut Receiver, $($Arg),*) -> &'a mut R + 'env, - F: for<'a> FnMut(&'a mut Receiver, $($Arg::Item<'a>),*) -> &'a mut R + 'env, - { - fn into_function(mut self) -> $crate::func::DynamicFunction<'env> { - const COUNT: usize = count_tts!(Receiver $($Arg)*); - - let info = $crate::func::FunctionInfo::new() - .with_name(std::any::type_name::()) - .with_args({ - #[allow(unused_mut)] - let mut _index = 1; - vec![ - $crate::func::args::ArgInfo::new::<&mut Receiver>(0), - $($crate::func::args::ArgInfo::new::<$Arg>({ - _index += 1; - _index - 1 - }),)* - ] - }) - .with_return_info($crate::func::ReturnInfo::new::<&mut R>()); - - $crate::func::DynamicFunction::new(move |args, _info| { - if args.len() != COUNT { - return Err($crate::func::error::FunctionError::InvalidArgCount { - expected: COUNT, - received: args.len(), - }); - } - - let [receiver, $($arg,)*] = args.take().try_into().expect("invalid number of arguments"); - - let receiver = receiver.take_mut::(_info.args().get(0).expect("argument index out of bounds"))?; - - #[allow(unused_mut)] - let mut _index = 1; - let ($($arg,)*) = ($($Arg::from_arg($arg, { - _index += 1; - _info.args().get(_index - 1).expect("argument index out of bounds") - })?,)*); - Ok($crate::func::Return::Mut((self)(receiver, $($arg,)*))) - }, info) - } - } - - // === Mut Receiver + Ref Return === // - impl<'env, Receiver, $($Arg,)* R, F> $crate::func::IntoFunction<'env, fn(&mut Receiver, $($Arg),*) -> fn(&mut R) -> &R> for F - where - Receiver: $crate::Reflect + $crate::TypePath, - for<'a> &'a mut Receiver: $crate::func::args::GetOwnership, - R: $crate::Reflect + $crate::TypePath, - for<'a> &'a mut R: $crate::func::args::GetOwnership, - $($Arg: $crate::func::args::FromArg + $crate::func::args::GetOwnership + $crate::TypePath,)* - F: for<'a> FnMut(&'a mut Receiver, $($Arg),*) -> &'a R + 'env, - F: for<'a> FnMut(&'a mut Receiver, $($Arg::Item<'a>),*) -> &'a R + 'env, - { - fn into_function(mut self) -> $crate::func::DynamicFunction<'env> { - const COUNT: usize = count_tts!(Receiver $($Arg)*); - - let info = $crate::func::FunctionInfo::new() - .with_name(std::any::type_name::()) - .with_args({ - #[allow(unused_mut)] - let mut _index = 1; - vec![ - $crate::func::args::ArgInfo::new::<&mut Receiver>(0), - $($crate::func::args::ArgInfo::new::<$Arg>({ - _index += 1; - _index - 1 - }),)* - ] - }) - .with_return_info($crate::func::ReturnInfo::new::<&mut R>()); - - $crate::func::DynamicFunction::new(move |args, _info| { - if args.len() != COUNT { - return Err($crate::func::error::FunctionError::InvalidArgCount { - expected: COUNT, - received: args.len(), - }); - } - - let [receiver, $($arg,)*] = args.take().try_into().expect("invalid number of arguments"); - - let receiver = receiver.take_mut::(_info.args().get(0).expect("argument index out of bounds"))?; - - #[allow(unused_mut)] - let mut _index = 1; - let ($($arg,)*) = ($($Arg::from_arg($arg, { - _index += 1; - _info.args().get(_index - 1).expect("argument index out of bounds") - })?,)*); - Ok($crate::func::Return::Ref((self)(receiver, $($arg,)*))) - }, info) - } - } - }; + let func = add.into_function(); + let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + let result = func.call(args).unwrap().unwrap_owned(); + assert_eq!(result.try_downcast_ref::(), Some(&100)); + } + + #[test] + fn should_default_closure_name_to_none() { + let c = 23; + let func = (|a: i32, b: i32| a + b + c).into_function(); + assert_eq!(func.info().name(), None); + } } - -all_tuples!(impl_into_function, 0, 15, Arg, arg); diff --git a/crates/bevy_reflect/src/func/into_function_mut.rs b/crates/bevy_reflect/src/func/into_function_mut.rs new file mode 100644 index 00000000000000..b4671047a658fd --- /dev/null +++ b/crates/bevy_reflect/src/func/into_function_mut.rs @@ -0,0 +1,83 @@ +use crate::func::{DynamicFunctionMut, ReflectFnMut, TypedFunction}; + +/// A trait for types that can be converted into a [`DynamicFunctionMut`]. +/// +/// This trait is automatically implemented for any type that implements +/// [`ReflectFnMut`] and [`TypedFunction`]. +/// +/// This trait can be seen as a superset of [`IntoFunction`]. +/// +/// See the [module-level documentation] for more information. +/// +/// # Trait Parameters +/// +/// This trait has a `Marker` type parameter that is used to get around issues with +/// [unconstrained type parameters] when defining impls with generic arguments or return types. +/// This `Marker` can be any type, provided it doesn't conflict with other implementations. +/// +/// Additionally, it has a lifetime parameter, `'env`, that is used to bound the lifetime of the function. +/// For named functions and some closures, this will end up just being `'static`, +/// however, closures that borrow from their environment will have a lifetime bound to that environment. +/// +/// [`IntoFunction`]: crate::func::IntoFunction +/// [module-level documentation]: crate::func +/// [unconstrained type parameters]: https://doc.rust-lang.org/error_codes/E0207.html +pub trait IntoFunctionMut<'env, Marker> { + /// Converts [`Self`] into a [`DynamicFunctionMut`]. + fn into_function_mut(self) -> DynamicFunctionMut<'env>; +} + +impl<'env, F, Marker1, Marker2> IntoFunctionMut<'env, (Marker1, Marker2)> for F +where + F: ReflectFnMut<'env, Marker1> + TypedFunction + 'env, +{ + fn into_function_mut(mut self) -> DynamicFunctionMut<'env> { + DynamicFunctionMut::new( + move |args| self.reflect_call_mut(args), + Self::function_info(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::func::{ArgList, IntoFunction}; + + #[test] + fn should_create_dynamic_function_mut_from_closure() { + let c = 23; + let func = (|a: i32, b: i32| a + b + c).into_function(); + let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + let result = func.call(args).unwrap().unwrap_owned(); + assert_eq!(result.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_create_dynamic_function_mut_from_closure_with_mutable_capture() { + let mut total = 0; + let func = (|a: i32, b: i32| total = a + b).into_function_mut(); + let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + func.call_once(args).unwrap(); + assert_eq!(total, 100); + } + + #[test] + fn should_create_dynamic_function_mut_from_function() { + fn add(a: i32, b: i32) -> i32 { + a + b + } + + let mut func = add.into_function_mut(); + let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); + let result = func.call(args).unwrap().unwrap_owned(); + assert_eq!(result.try_downcast_ref::(), Some(&100)); + } + + #[test] + fn should_default_closure_name_to_none() { + let mut total = 0; + let func = (|a: i32, b: i32| total = a + b).into_function_mut(); + assert_eq!(func.info().name(), None); + } +} diff --git a/crates/bevy_reflect/src/func/macros.rs b/crates/bevy_reflect/src/func/macros.rs index a6be3850e3d1e3..3fb93a22306109 100644 --- a/crates/bevy_reflect/src/func/macros.rs +++ b/crates/bevy_reflect/src/func/macros.rs @@ -97,3 +97,16 @@ macro_rules! impl_function_traits { } pub(crate) use impl_function_traits; + +/// Helper macro that returns the number of tokens it receives. +/// +/// See [here] for details. +/// +/// [here]: https://veykril.github.io/tlborm/decl-macros/building-blocks/counting.html#bit-twiddling +macro_rules! count_tokens { + () => { 0 }; + ($odd:tt $($a:tt $b:tt)*) => { ($crate::func::macros::count_tokens!($($a)*) << 1) | 1 }; + ($($a:tt $even:tt)*) => { $crate::func::macros::count_tokens!($($a)*) << 1 }; +} + +pub(crate) use count_tokens; diff --git a/crates/bevy_reflect/src/func/mod.rs b/crates/bevy_reflect/src/func/mod.rs index a964cf60f7a226..a924231ff4cabe 100644 --- a/crates/bevy_reflect/src/func/mod.rs +++ b/crates/bevy_reflect/src/func/mod.rs @@ -1,22 +1,21 @@ //! Reflection-based dynamic functions. //! //! This module provides a way to pass around and call functions dynamically -//! using the [`DynamicFunction`] type. +//! using the [`DynamicFunction`] and [`DynamicFunctionMut`] types. //! -//! Many simple functions and closures can be automatically converted to [`DynamicFunction`] -//! using the [`IntoFunction`] trait. +//! Many simple functions and closures can be automatically converted to these types +//! using the [`IntoFunction`] and [`IntoFunctionMut`] traits, respectively. //! -//! Once the [`DynamicFunction`] is created, it can be called with a set of arguments provided +//! Once this dynamic representation is created, it can be called with a set of arguments provided //! via an [`ArgList`]. //! //! This returns a [`FunctionResult`] containing the [`Return`] value, -//! which can be used to extract a [`Reflect`] trait object. -//! +//! which can be used to extract a [`PartialReflect`] trait object. //! //! # Example //! //! ``` -//! # use bevy_reflect::Reflect; +//! # use bevy_reflect::PartialReflect; //! # use bevy_reflect::func::args::ArgList; //! # use bevy_reflect::func::{DynamicFunction, FunctionResult, IntoFunction, Return}; //! fn add(a: i32, b: i32) -> i32 { @@ -28,175 +27,122 @@ //! // Pushing a known type with owned ownership //! .push_owned(25_i32) //! // Pushing a reflected type with owned ownership -//! .push_boxed(Box::new(75_i32) as Box); +//! .push_boxed(Box::new(75_i32) as Box); //! let result: FunctionResult = func.call(args); //! let value: Return = result.unwrap(); -//! assert_eq!(value.unwrap_owned().downcast_ref::(), Some(&100)); +//! assert_eq!(value.unwrap_owned().try_downcast_ref::(), Some(&100)); +//! ``` +//! +//! # Types of Functions +//! +//! For simplicity, this module uses the umbrella term "function" to refer to any Rust callable: +//! code that can be invoked with a set of arguments to perform some action. +//! +//! In Rust, there are two main categories of callables: functions and closures. +//! +//! A "function" is a callable that does not capture its environment. +//! These are typically defined with the `fn` keyword, which are referred to as _named_ functions. +//! But they are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. +//! +//! ```rust +//! // This is a named function: +//! fn add(a: i32, b: i32) -> i32 { +//! a + b +//! } +//! +//! // This is an anonymous function: +//! let add = |a: i32, b: i32| a + b; //! ``` //! +//! Closures, on the other hand, are special functions that do capture their environment. +//! These are always defined with anonymous function syntax. +//! +//! ```rust +//! // A closure that captures an immutable reference to a variable +//! let c = 123; +//! let add = |a: i32, b: i32| a + b + c; +//! +//! // A closure that captures a mutable reference to a variable +//! let mut total = 0; +//! let add = |a: i32, b: i32| total += a + b; +//! +//! // A closure that takes ownership of its captured variables by moving them +//! let c = 123; +//! let add = move |a: i32, b: i32| a + b + c; +//! ``` +//! +//! # Valid Signatures +//! +//! Many of the traits in this module have default blanket implementations over a specific set of function signatures. +//! +//! These signatures are: +//! - `(...) -> R` +//! - `for<'a> (&'a arg, ...) -> &'a R` +//! - `for<'a> (&'a mut arg, ...) -> &'a R` +//! - `for<'a> (&'a mut arg, ...) -> &'a mut R` +//! +//! Where `...` represents 0 to 15 arguments (inclusive) of the form `T`, `&T`, or `&mut T`. +//! The lifetime of any reference to the return type `R`, must be tied to a "receiver" argument +//! (i.e. the first argument in the signature, normally `self`). +//! +//! Each trait will also have its own requirements for what traits are required for both arguments and return types, +//! but a good rule-of-thumb is that all types should derive [`Reflect`]. +//! +//! The reason for such a small subset of valid signatures is due to limitations in Rust— +//! namely the [lack of variadic generics] and certain [coherence issues]. +//! +//! For other functions that don't conform to one of the above signatures, +//! [`DynamicFunction`] and [`DynamicFunctionMut`] can instead be created manually. +//! +//! [`PartialReflect`]: crate::PartialReflect //! [`Reflect`]: crate::Reflect +//! [lack of variadic generics]: https://poignardazur.github.io/2024/05/25/report-on-rustnl-variadics/ +//! [coherence issues]: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#coherence-leak-check +pub use args::{ArgError, ArgList, ArgValue}; +pub use dynamic_function::*; +pub use dynamic_function_mut::*; pub use error::*; -pub use function::*; pub use info::*; pub use into_function::*; +pub use into_function_mut::*; +pub use reflect_fn::*; +pub use reflect_fn_mut::*; +pub use registry::*; pub use return_type::*; -pub use args::{Arg, ArgError, ArgList}; - pub mod args; +mod dynamic_function; +mod dynamic_function_mut; mod error; -mod function; mod info; mod into_function; +mod into_function_mut; pub(crate) mod macros; +mod reflect_fn; +mod reflect_fn_mut; +mod registry; mod return_type; #[cfg(test)] mod tests { - use super::*; - use crate as bevy_reflect; - use crate::func::args::{ArgError, ArgId, ArgList, Ownership}; - use crate::{Reflect, TypePath}; use alloc::borrow::Cow; - #[test] - fn should_create_dynamic_function() { - fn add(a: i32, b: i32) -> i32 { - a + b - } - - let mut func = add.into_function(); - let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); - let result = func.call(args).unwrap().unwrap_owned(); - assert_eq!(result.downcast_ref::(), Some(&100)); - } - - #[test] - fn should_create_dynamic_closure() { - let mut func = (|a: i32, b: i32| a + b).into_function(); - let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); - let result = func.call(args).unwrap().unwrap_owned(); - assert_eq!(result.downcast_ref::(), Some(&100)); - } - - #[test] - fn should_create_dynamic_method() { - #[derive(Reflect, Debug, PartialEq)] - struct Foo(i32); - - impl Foo { - pub fn add(&self, other: &Foo) -> Foo { - Foo(self.0 + other.0) - } - } - - let foo_a = Foo(25); - let foo_b = Foo(75); - - let mut func = Foo::add.into_function(); - let args = ArgList::new().push_ref(&foo_a).push_ref(&foo_b); - let result = func.call(args).unwrap().unwrap_owned(); - assert_eq!(result.downcast_ref::(), Some(&Foo(100))); - } - - #[test] - fn should_allow_zero_args() { - fn foo() -> String { - String::from("Hello, World!") - } - - let mut func = foo.into_function(); - let args = ArgList::new(); - let result = func.call(args).unwrap().unwrap_owned(); - assert_eq!( - result.downcast_ref::(), - Some(&String::from("Hello, World!")) - ); - } - - #[test] - fn should_allow_unit_return() { - fn foo(_: i32) {} - - let mut func = foo.into_function(); - let args = ArgList::new().push_owned(123_i32); - let result = func.call(args).unwrap(); - assert!(result.is_unit()); - } - - #[test] - fn should_allow_reference_return() { - fn foo<'a>(value: &'a i32, _: String, _: &bool) -> &'a i32 { - value - } - - let value: i32 = 123; - let mut func = foo.into_function(); - let args = ArgList::new() - .push_ref(&value) - .push_owned(String::from("Hello, World!")) - .push_ref(&true); - let result = func.call(args).unwrap().unwrap_ref(); - assert_eq!(result.downcast_ref::(), Some(&123)); - } - - #[test] - fn should_allow_mutable_reference_return() { - fn foo<'a>(value: &'a mut i32, _: String, _: &bool) -> &'a mut i32 { - value - } + use crate::func::args::{ArgError, ArgList, Ownership}; + use crate::TypePath; - let mut value: i32 = 123; - let mut func = foo.into_function(); - let args = ArgList::new() - .push_mut(&mut value) - .push_owned(String::from("Hello, World!")) - .push_ref(&true); - let result = func.call(args).unwrap().unwrap_mut(); - assert_eq!(result.downcast_mut::(), Some(&mut 123)); - } - - #[test] - fn should_default_with_function_type_name() { - fn foo() {} - - let func = foo.into_function(); - assert_eq!( - func.info().name(), - Some("bevy_reflect::func::tests::should_default_with_function_type_name::foo") - ); - } - - #[test] - fn should_default_with_closure_type_name() { - let bar = |_: i32| {}; - - let func = bar.into_function(); - assert_eq!( - func.info().name(), - Some("bevy_reflect::func::tests::should_default_with_closure_type_name::{{closure}}") - ); - } - - #[test] - fn should_overwrite_function_name() { - fn foo() {} - - let func = foo.into_function().with_name("my_function"); - assert_eq!(func.info().name(), Some("my_function")); - } + use super::*; #[test] fn should_error_on_missing_args() { fn foo(_: i32) {} - let mut func = foo.into_function(); + let func = foo.into_function(); let args = ArgList::new(); let result = func.call(args); assert_eq!( result.unwrap_err(), - FunctionError::InvalidArgCount { + FunctionError::ArgCountMismatch { expected: 1, received: 0 } @@ -207,12 +153,12 @@ mod tests { fn should_error_on_too_many_args() { fn foo() {} - let mut func = foo.into_function(); + let func = foo.into_function(); let args = ArgList::new().push_owned(123_i32); let result = func.call(args); assert_eq!( result.unwrap_err(), - FunctionError::InvalidArgCount { + FunctionError::ArgCountMismatch { expected: 0, received: 1 } @@ -223,13 +169,13 @@ mod tests { fn should_error_on_invalid_arg_type() { fn foo(_: i32) {} - let mut func = foo.into_function(); + let func = foo.into_function(); let args = ArgList::new().push_owned(123_u32); let result = func.call(args); assert_eq!( result.unwrap_err(), FunctionError::ArgError(ArgError::UnexpectedType { - id: ArgId::Index(0), + index: 0, expected: Cow::Borrowed(i32::type_path()), received: Cow::Borrowed(u32::type_path()) }) @@ -240,26 +186,16 @@ mod tests { fn should_error_on_invalid_arg_ownership() { fn foo(_: &i32) {} - let mut func = foo.into_function(); + let func = foo.into_function(); let args = ArgList::new().push_owned(123_i32); let result = func.call(args); assert_eq!( result.unwrap_err(), FunctionError::ArgError(ArgError::InvalidOwnership { - id: ArgId::Index(0), + index: 0, expected: Ownership::Ref, received: Ownership::Owned }) ); } - - #[test] - fn should_convert_dynamic_function_with_into_function() { - fn make_function<'a, F: IntoFunction<'a, M>, M>(f: F) -> DynamicFunction<'a> { - f.into_function() - } - - let function: DynamicFunction = make_function(|| {}); - let _: DynamicFunction = make_function(function); - } } diff --git a/crates/bevy_reflect/src/func/reflect_fn.rs b/crates/bevy_reflect/src/func/reflect_fn.rs new file mode 100644 index 00000000000000..1dc1d313023091 --- /dev/null +++ b/crates/bevy_reflect/src/func/reflect_fn.rs @@ -0,0 +1,197 @@ +use bevy_utils::all_tuples; + +use crate::func::args::FromArg; +use crate::func::macros::count_tokens; +use crate::func::{ArgList, FunctionError, FunctionResult, IntoReturn, ReflectFnMut}; +use crate::{Reflect, TypePath}; + +/// A reflection-based version of the [`Fn`] trait. +/// +/// This allows functions to be called dynamically through [reflection]. +/// +/// # Blanket Implementation +/// +/// This trait has a blanket implementation that covers: +/// - Functions and methods defined with the `fn` keyword +/// - Anonymous functions +/// - Function pointers +/// - Closures that capture immutable references to their environment +/// - Closures that take ownership of captured variables +/// +/// For each of the above cases, the function signature may only have up to 15 arguments, +/// not including an optional receiver argument (often `&self` or `&mut self`). +/// This optional receiver argument may be either a mutable or immutable reference to a type. +/// If the return type is also a reference, its lifetime will be bound to the lifetime of this receiver. +/// +/// See the [module-level documentation] for more information on valid signatures. +/// +/// To handle functions that capture mutable references to their environment, +/// see the [`ReflectFnMut`] trait instead. +/// +/// Arguments are expected to implement [`FromArg`], and the return type is expected to implement [`IntoReturn`]. +/// Both of these traits are automatically implemented when using the `Reflect` [derive macro]. +/// +/// # Example +/// +/// ``` +/// # use bevy_reflect::func::{ArgList, FunctionInfo, ReflectFn}; +/// # +/// fn add(a: i32, b: i32) -> i32 { +/// a + b +/// } +/// +/// let args = ArgList::new().push_owned(25_i32).push_owned(75_i32); +/// +/// let value = add.reflect_call(args).unwrap().unwrap_owned(); +/// assert_eq!(value.try_take::().unwrap(), 100); +/// ``` +/// +/// # Trait Parameters +/// +/// This trait has a `Marker` type parameter that is used to get around issues with +/// [unconstrained type parameters] when defining impls with generic arguments or return types. +/// This `Marker` can be any type, provided it doesn't conflict with other implementations. +/// +/// Additionally, it has a lifetime parameter, `'env`, that is used to bound the lifetime of the function. +/// For named functions and some closures, this will end up just being `'static`, +/// however, closures that borrow from their environment will have a lifetime bound to that environment. +/// +/// [reflection]: crate +/// [module-level documentation]: crate::func +/// [derive macro]: derive@crate::Reflect +/// [unconstrained type parameters]: https://doc.rust-lang.org/error_codes/E0207.html +pub trait ReflectFn<'env, Marker>: ReflectFnMut<'env, Marker> { + /// Call the function with the given arguments and return the result. + fn reflect_call<'a>(&self, args: ArgList<'a>) -> FunctionResult<'a>; +} + +/// Helper macro for implementing [`ReflectFn`] on Rust functions. +/// +/// This currently implements it for the following signatures (where `argX` may be any of `T`, `&T`, or `&mut T`): +/// - `Fn(arg0, arg1, ..., argN) -> R` +/// - `Fn(&Receiver, arg0, arg1, ..., argN) -> &R` +/// - `Fn(&mut Receiver, arg0, arg1, ..., argN) -> &mut R` +/// - `Fn(&mut Receiver, arg0, arg1, ..., argN) -> &R` +macro_rules! impl_reflect_fn { + ($(($Arg:ident, $arg:ident)),*) => { + // === (...) -> ReturnType === // + impl<'env, $($Arg,)* ReturnType, Function> ReflectFn<'env, fn($($Arg),*) -> [ReturnType]> for Function + where + $($Arg: FromArg,)* + // This clause allows us to convert `ReturnType` into `Return` + ReturnType: IntoReturn + Reflect, + Function: Fn($($Arg),*) -> ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> Fn($($Arg::This<'a>),*) -> ReturnType + 'env, + { + #[allow(unused_mut)] + fn reflect_call<'a>(&self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!($($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)($($arg,)*).into_return()) + } + } + + // === (&self, ...) -> &ReturnType === // + impl<'env, Receiver, $($Arg,)* ReturnType, Function> ReflectFn<'env, fn(&Receiver, $($Arg),*) -> &ReturnType> for Function + where + Receiver: Reflect + TypePath, + $($Arg: FromArg,)* + ReturnType: Reflect, + // This clause allows us to convert `&ReturnType` into `Return` + for<'a> &'a ReturnType: IntoReturn, + Function: for<'a> Fn(&'a Receiver, $($Arg),*) -> &'a ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> Fn(&'a Receiver, $($Arg::This<'a>),*) -> &'a ReturnType + 'env, + { + fn reflect_call<'a>(&self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!(Receiver $($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + let receiver = args.take_ref::()?; + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)(receiver, $($arg,)*).into_return()) + } + } + + // === (&mut self, ...) -> &mut ReturnType === // + impl<'env, Receiver, $($Arg,)* ReturnType, Function> ReflectFn<'env, fn(&mut Receiver, $($Arg),*) -> &mut ReturnType> for Function + where + Receiver: Reflect + TypePath, + $($Arg: FromArg,)* + ReturnType: Reflect, + // This clause allows us to convert `&mut ReturnType` into `Return` + for<'a> &'a mut ReturnType: IntoReturn, + Function: for<'a> Fn(&'a mut Receiver, $($Arg),*) -> &'a mut ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> Fn(&'a mut Receiver, $($Arg::This<'a>),*) -> &'a mut ReturnType + 'env, + { + fn reflect_call<'a>(&self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!(Receiver $($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + let receiver = args.take_mut::()?; + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)(receiver, $($arg,)*).into_return()) + } + } + + // === (&mut self, ...) -> &ReturnType === // + impl<'env, Receiver, $($Arg,)* ReturnType, Function> ReflectFn<'env, fn(&mut Receiver, $($Arg),*) -> &ReturnType> for Function + where + Receiver: Reflect + TypePath, + $($Arg: FromArg,)* + ReturnType: Reflect, + // This clause allows us to convert `&ReturnType` into `Return` + for<'a> &'a ReturnType: IntoReturn, + Function: for<'a> Fn(&'a mut Receiver, $($Arg),*) -> &'a ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> Fn(&'a mut Receiver, $($Arg::This<'a>),*) -> &'a ReturnType + 'env, + { + fn reflect_call<'a>(&self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!(Receiver $($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + let receiver = args.take_mut::()?; + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)(receiver, $($arg,)*).into_return()) + } + } + }; +} + +all_tuples!(impl_reflect_fn, 0, 15, Arg, arg); diff --git a/crates/bevy_reflect/src/func/reflect_fn_mut.rs b/crates/bevy_reflect/src/func/reflect_fn_mut.rs new file mode 100644 index 00000000000000..a6a6bd6e9fa58e --- /dev/null +++ b/crates/bevy_reflect/src/func/reflect_fn_mut.rs @@ -0,0 +1,204 @@ +use bevy_utils::all_tuples; + +use crate::func::args::FromArg; +use crate::func::macros::count_tokens; +use crate::func::{ArgList, FunctionError, FunctionResult, IntoReturn}; +use crate::{Reflect, TypePath}; + +/// A reflection-based version of the [`FnMut`] trait. +/// +/// This allows functions to be called dynamically through [reflection]. +/// +/// This is a supertrait of [`ReflectFn`], and is used for functions that may mutate their environment, +/// such as closures that capture mutable references. +/// +/// # Blanket Implementation +/// +/// This trait has a blanket implementation that covers everything that [`ReflectFn`] does: +/// - Functions and methods defined with the `fn` keyword +/// - Anonymous functions +/// - Function pointers +/// - Closures that capture immutable references to their environment +/// - Closures that take ownership of captured variables +/// +/// But also allows for: +/// - Closures that capture mutable references to their environment +/// +/// For each of the above cases, the function signature may only have up to 15 arguments, +/// not including an optional receiver argument (often `&self` or `&mut self`). +/// This optional receiver argument may be either a mutable or immutable reference to a type. +/// If the return type is also a reference, its lifetime will be bound to the lifetime of this receiver. +/// +/// See the [module-level documentation] for more information on valid signatures. +/// +/// Arguments are expected to implement [`FromArg`], and the return type is expected to implement [`IntoReturn`]. +/// Both of these traits are automatically implemented when using the `Reflect` [derive macro]. +/// +/// # Example +/// +/// ``` +/// # use bevy_reflect::func::{ArgList, FunctionInfo, ReflectFnMut}; +/// # +/// let mut list: Vec = vec![1, 3]; +/// +/// // `insert` is a closure that captures a mutable reference to `list` +/// let mut insert = |index: usize, value: i32| { +/// list.insert(index, value); +/// }; +/// +/// let args = ArgList::new().push_owned(1_usize).push_owned(2_i32); +/// +/// insert.reflect_call_mut(args).unwrap(); +/// assert_eq!(list, vec![1, 2, 3]); +/// ``` +/// +/// # Trait Parameters +/// +/// This trait has a `Marker` type parameter that is used to get around issues with +/// [unconstrained type parameters] when defining impls with generic arguments or return types. +/// This `Marker` can be any type, provided it doesn't conflict with other implementations. +/// +/// Additionally, it has a lifetime parameter, `'env`, that is used to bound the lifetime of the function. +/// For named functions and some closures, this will end up just being `'static`, +/// however, closures that borrow from their environment will have a lifetime bound to that environment. +/// +/// [reflection]: crate +/// [`ReflectFn`]: crate::func::ReflectFn +/// [module-level documentation]: crate::func +/// [derive macro]: derive@crate::Reflect +/// [unconstrained type parameters]: https://doc.rust-lang.org/error_codes/E0207.html +pub trait ReflectFnMut<'env, Marker> { + /// Call the function with the given arguments and return the result. + fn reflect_call_mut<'a>(&mut self, args: ArgList<'a>) -> FunctionResult<'a>; +} + +/// Helper macro for implementing [`ReflectFnMut`] on Rust functions. +/// +/// This currently implements it for the following signatures (where `argX` may be any of `T`, `&T`, or `&mut T`): +/// - `FnMut(arg0, arg1, ..., argN) -> R` +/// - `FnMut(&Receiver, arg0, arg1, ..., argN) -> &R` +/// - `FnMut(&mut Receiver, arg0, arg1, ..., argN) -> &mut R` +/// - `FnMut(&mut Receiver, arg0, arg1, ..., argN) -> &R` +macro_rules! impl_reflect_fn_mut { + ($(($Arg:ident, $arg:ident)),*) => { + // === (...) -> ReturnType === // + impl<'env, $($Arg,)* ReturnType, Function> ReflectFnMut<'env, fn($($Arg),*) -> [ReturnType]> for Function + where + $($Arg: FromArg,)* + // This clause allows us to convert `ReturnType` into `Return` + ReturnType: IntoReturn + Reflect, + Function: FnMut($($Arg),*) -> ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> FnMut($($Arg::This<'a>),*) -> ReturnType + 'env, + { + #[allow(unused_mut)] + fn reflect_call_mut<'a>(&mut self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!($($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)($($arg,)*).into_return()) + } + } + + // === (&self, ...) -> &ReturnType === // + impl<'env, Receiver, $($Arg,)* ReturnType, Function> ReflectFnMut<'env, fn(&Receiver, $($Arg),*) -> &ReturnType> for Function + where + Receiver: Reflect + TypePath, + $($Arg: FromArg,)* + ReturnType: Reflect, + // This clause allows us to convert `&ReturnType` into `Return` + for<'a> &'a ReturnType: IntoReturn, + Function: for<'a> FnMut(&'a Receiver, $($Arg),*) -> &'a ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> FnMut(&'a Receiver, $($Arg::This<'a>),*) -> &'a ReturnType + 'env, + { + fn reflect_call_mut<'a>(&mut self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!(Receiver $($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + let receiver = args.take_ref::()?; + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)(receiver, $($arg,)*).into_return()) + } + } + + // === (&mut self, ...) -> &mut ReturnType === // + impl<'env, Receiver, $($Arg,)* ReturnType, Function> ReflectFnMut<'env, fn(&mut Receiver, $($Arg),*) -> &mut ReturnType> for Function + where + Receiver: Reflect + TypePath, + $($Arg: FromArg,)* + ReturnType: Reflect, + // This clause allows us to convert `&mut ReturnType` into `Return` + for<'a> &'a mut ReturnType: IntoReturn, + Function: for<'a> FnMut(&'a mut Receiver, $($Arg),*) -> &'a mut ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> FnMut(&'a mut Receiver, $($Arg::This<'a>),*) -> &'a mut ReturnType + 'env, + { + fn reflect_call_mut<'a>(&mut self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!(Receiver $($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + let receiver = args.take_mut::()?; + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)(receiver, $($arg,)*).into_return()) + } + } + + // === (&mut self, ...) -> &ReturnType === // + impl<'env, Receiver, $($Arg,)* ReturnType, Function> ReflectFnMut<'env, fn(&mut Receiver, $($Arg),*) -> &ReturnType> for Function + where + Receiver: Reflect + TypePath, + $($Arg: FromArg,)* + ReturnType: Reflect, + // This clause allows us to convert `&ReturnType` into `Return` + for<'a> &'a ReturnType: IntoReturn, + Function: for<'a> FnMut(&'a mut Receiver, $($Arg),*) -> &'a ReturnType + 'env, + // This clause essentially asserts that `Arg::This` is the same type as `Arg` + Function: for<'a> FnMut(&'a mut Receiver, $($Arg::This<'a>),*) -> &'a ReturnType + 'env, + { + fn reflect_call_mut<'a>(&mut self, mut args: ArgList<'a>) -> FunctionResult<'a> { + const COUNT: usize = count_tokens!(Receiver $($Arg)*); + + if args.len() != COUNT { + return Err(FunctionError::ArgCountMismatch { + expected: COUNT, + received: args.len(), + }); + } + + // Extract all arguments (in order) + let receiver = args.take_mut::()?; + $(let $arg = args.take::<$Arg>()?;)* + + Ok((self)(receiver, $($arg,)*).into_return()) + } + } + }; +} + +all_tuples!(impl_reflect_fn_mut, 0, 15, Arg, arg); diff --git a/crates/bevy_reflect/src/func/registry.rs b/crates/bevy_reflect/src/func/registry.rs new file mode 100644 index 00000000000000..87f66a070da04f --- /dev/null +++ b/crates/bevy_reflect/src/func/registry.rs @@ -0,0 +1,493 @@ +use alloc::borrow::Cow; +use core::fmt::Debug; +use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +use bevy_utils::HashMap; + +use crate::func::{DynamicFunction, FunctionRegistrationError, IntoFunction}; + +/// A registry of [reflected functions]. +/// +/// This is the function-equivalent to the [`TypeRegistry`]. +/// +/// All functions must be `'static` as they are stored as [`DynamicFunction<'static>`]. +/// +/// [reflected functions]: crate::func +/// [`TypeRegistry`]: crate::TypeRegistry +#[derive(Default)] +pub struct FunctionRegistry { + /// Maps function [names] to their respective [`DynamicFunctions`]. + /// + /// [names]: DynamicFunction::name + /// [`DynamicFunctions`]: DynamicFunction + functions: HashMap, DynamicFunction<'static>>, +} + +impl FunctionRegistry { + /// Attempts to register the given function. + /// + /// This function accepts both functions that satisfy [`IntoFunction`] + /// and direct [`DynamicFunction`] instances. + /// The given function will internally be stored as a [`DynamicFunction<'static>`] + /// and mapped according to its [name]. + /// + /// Because the function must have a name, + /// anonymous functions (e.g. `|a: i32, b: i32| { a + b }`) and closures must instead + /// be registered using [`register_with_name`] or manually converted to a [`DynamicFunction`] + /// and named using [`DynamicFunction::with_name`]. + /// Failure to do so will result in an error being returned. + /// + /// If a registered function with the same name already exists, + /// it will not be registered again and an error will be returned. + /// To register the function anyway, overwriting any existing registration, + /// use [`overwrite_registration`] instead. + /// + /// # Examples + /// + /// ``` + /// # use bevy_reflect::func::{FunctionRegistrationError, FunctionRegistry}; + /// fn add(a: i32, b: i32) -> i32 { + /// a + b + /// } + /// + /// # fn main() -> Result<(), FunctionRegistrationError> { + /// let mut registry = FunctionRegistry::default(); + /// registry.register(add)?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Functions cannot be registered more than once. + /// + /// ``` + /// # use bevy_reflect::func::{FunctionRegistrationError, FunctionRegistry, IntoFunction}; + /// fn add(a: i32, b: i32) -> i32 { + /// a + b + /// } + /// + /// let mut registry = FunctionRegistry::default(); + /// registry.register(add).unwrap(); + /// + /// let result = registry.register(add); + /// assert!(matches!(result, Err(FunctionRegistrationError::DuplicateName(_)))); + /// + /// // Note that this simply relies on the name of the function to determine uniqueness. + /// // You can rename the function to register a separate instance of it. + /// let result = registry.register(add.into_function().with_name("add2")); + /// assert!(result.is_ok()); + /// ``` + /// + /// Anonymous functions and closures should be registered using [`register_with_name`] or given a name using [`DynamicFunction::with_name`]. + /// + /// ``` + /// # use bevy_reflect::func::{FunctionRegistrationError, FunctionRegistry, IntoFunction}; + /// + /// let anonymous = || -> i32 { 123 }; + /// + /// let mut registry = FunctionRegistry::default(); + /// + /// let result = registry.register(|a: i32, b: i32| a + b); + /// assert!(matches!(result, Err(FunctionRegistrationError::MissingName))); + /// + /// let result = registry.register_with_name("my_crate::add", |a: i32, b: i32| a + b); + /// assert!(result.is_ok()); + /// + /// let result = registry.register((|a: i32, b: i32| a * b).into_function().with_name("my_crate::mul")); + /// assert!(result.is_ok()); + /// ``` + /// + /// [name]: DynamicFunction::name + /// [`register_with_name`]: Self::register_with_name + /// [`overwrite_registration`]: Self::overwrite_registration + pub fn register( + &mut self, + function: F, + ) -> Result<&mut Self, FunctionRegistrationError> + where + F: IntoFunction<'static, Marker> + 'static, + { + let function = function.into_function(); + let name = function + .name() + .ok_or(FunctionRegistrationError::MissingName)? + .clone(); + self.functions + .try_insert(name, function.into_function()) + .map_err(|err| FunctionRegistrationError::DuplicateName(err.entry.key().clone()))?; + + Ok(self) + } + + /// Attempts to register the given function with the given name. + /// + /// This function accepts both functions that satisfy [`IntoFunction`] + /// and direct [`DynamicFunction`] instances. + /// The given function will internally be stored as a [`DynamicFunction<'static>`] + /// with its [name] set to the given name. + /// + /// For named functions (e.g. `fn add(a: i32, b: i32) -> i32 { a + b }`) where a custom name is not needed, + /// it's recommended to use [`register`] instead as the generated name is guaranteed to be unique. + /// + /// If a registered function with the same name already exists, + /// it will not be registered again and an error will be returned. + /// To register the function anyway, overwriting any existing registration, + /// use [`overwrite_registration_with_name`] instead. + /// + /// To avoid conflicts, it's recommended to use a unique name for the function. + /// This can be achieved by "namespacing" the function with a unique identifier, + /// such as the name of your crate. + /// + /// For example, to register a function, `add`, from a crate, `my_crate`, + /// you could use the name, `"my_crate::add"`. + /// + /// Another approach could be to use the [type name] of the function, + /// however, it should be noted that anonymous functions and closures + ///are not guaranteed to have unique type names. + /// + /// This method is a convenience around calling [`IntoFunction::into_function`] and [`DynamicFunction::with_name`] + /// on the function and inserting it into the registry using the [`register`] method. + /// + /// # Examples + /// + /// ``` + /// # use bevy_reflect::func::{FunctionRegistrationError, FunctionRegistry}; + /// # fn main() -> Result<(), FunctionRegistrationError> { + /// fn mul(a: i32, b: i32) -> i32 { + /// a * b + /// } + /// + /// let div = |a: i32, b: i32| a / b; + /// + /// let mut registry = FunctionRegistry::default(); + /// registry + /// // Registering an anonymous function with a unique name + /// .register_with_name("my_crate::add", |a: i32, b: i32| { + /// a + b + /// })? + /// // Registering an existing function with its type name + /// .register_with_name(std::any::type_name_of_val(&mul), mul)? + /// // Registering an existing function with a custom name + /// .register_with_name("my_crate::mul", mul)?; + /// + /// // Be careful not to register anonymous functions with their type name. + /// // This code works but registers the function with a non-unique name like `foo::bar::{{closure}}` + /// registry.register_with_name(std::any::type_name_of_val(&div), div)?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Names must be unique. + /// + /// ```should_panic + /// # use bevy_reflect::func::FunctionRegistry; + /// fn one() {} + /// fn two() {} + /// + /// let mut registry = FunctionRegistry::default(); + /// registry.register_with_name("my_function", one).unwrap(); + /// + /// // Panic! A function has already been registered with the name "my_function" + /// registry.register_with_name("my_function", two).unwrap(); + /// ``` + /// + /// [name]: DynamicFunction::name + /// [`register`]: Self::register + /// [`overwrite_registration_with_name`]: Self::overwrite_registration_with_name + /// [type name]: std::any::type_name + pub fn register_with_name( + &mut self, + name: impl Into>, + function: F, + ) -> Result<&mut Self, FunctionRegistrationError> + where + F: IntoFunction<'static, Marker> + 'static, + { + let function = function.into_function().with_name(name); + self.register(function) + } + + /// Registers the given function, overwriting any existing registration. + /// + /// This function accepts both functions that satisfy [`IntoFunction`] + /// and direct [`DynamicFunction`] instances. + /// The given function will internally be stored as a [`DynamicFunction<'static>`] + /// and mapped according to its [name]. + /// + /// Because the function must have a name, + /// anonymous functions (e.g. `|a: i32, b: i32| { a + b }`) and closures must instead + /// be registered using [`overwrite_registration_with_name`] or manually converted to a [`DynamicFunction`] + /// and named using [`DynamicFunction::with_name`]. + /// Failure to do so will result in an error being returned. + /// + /// To avoid overwriting existing registrations, + /// it's recommended to use the [`register`] method instead. + /// + /// Returns the previous function with the same name, if any. + /// + /// [name]: DynamicFunction::name + /// [`overwrite_registration_with_name`]: Self::overwrite_registration_with_name + /// [`register`]: Self::register + pub fn overwrite_registration( + &mut self, + function: F, + ) -> Result>, FunctionRegistrationError> + where + F: IntoFunction<'static, Marker> + 'static, + { + let function = function.into_function(); + let name = function + .name() + .ok_or(FunctionRegistrationError::MissingName)? + .clone(); + + Ok(self.functions.insert(name, function)) + } + + /// Registers the given function, overwriting any existing registration. + /// + /// This function accepts both functions that satisfy [`IntoFunction`] + /// and direct [`DynamicFunction`] instances. + /// The given function will internally be stored as a [`DynamicFunction<'static>`] + /// with its [name] set to the given name. + /// + /// Functions are mapped according to their name. + /// To avoid overwriting existing registrations, + /// it's recommended to use the [`register_with_name`] method instead. + /// + /// This method is a convenience around calling [`IntoFunction::into_function`] and [`DynamicFunction::with_name`] + /// on the function and inserting it into the registry using the [`overwrite_registration`] method. + /// + /// Returns the previous function with the same name, if any. + /// + /// [name]: DynamicFunction::name + /// [`register_with_name`]: Self::register_with_name + /// [`overwrite_registration`]: Self::overwrite_registration + pub fn overwrite_registration_with_name( + &mut self, + name: impl Into>, + function: F, + ) -> Option> + where + F: IntoFunction<'static, Marker> + 'static, + { + let function = function.into_function().with_name(name); + match self.overwrite_registration(function) { + Ok(existing) => existing, + Err(FunctionRegistrationError::MissingName) => { + unreachable!("the function should have a name") + } + Err(FunctionRegistrationError::DuplicateName(_)) => { + unreachable!("should overwrite functions with the same name") + } + } + } + + /// Get a reference to a registered function by [name]. + /// + /// [name]: DynamicFunction::name + pub fn get(&self, name: &str) -> Option<&DynamicFunction<'static>> { + self.functions.get(name) + } + + /// Returns `true` if a function with the given [name] is registered. + /// + /// [name]: DynamicFunction::name + pub fn contains(&self, name: &str) -> bool { + self.functions.contains_key(name) + } + + /// Returns an iterator over all registered functions. + pub fn iter(&self) -> impl ExactSizeIterator> { + self.functions.values() + } + + /// Returns the number of registered functions. + pub fn len(&self) -> usize { + self.functions.len() + } + + /// Returns `true` if no functions are registered. + pub fn is_empty(&self) -> bool { + self.functions.is_empty() + } +} + +impl Debug for FunctionRegistry { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_set().entries(self.functions.values()).finish() + } +} + +/// A synchronized wrapper around a [`FunctionRegistry`]. +#[derive(Clone, Default, Debug)] +pub struct FunctionRegistryArc { + pub internal: Arc>, +} + +impl FunctionRegistryArc { + /// Takes a read lock on the underlying [`FunctionRegistry`]. + pub fn read(&self) -> RwLockReadGuard<'_, FunctionRegistry> { + self.internal.read().unwrap_or_else(PoisonError::into_inner) + } + + /// Takes a write lock on the underlying [`FunctionRegistry`]. + pub fn write(&self) -> RwLockWriteGuard<'_, FunctionRegistry> { + self.internal + .write() + .unwrap_or_else(PoisonError::into_inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::func::{ArgList, IntoFunction}; + + #[test] + fn should_register_function() { + fn foo() -> i32 { + 123 + } + + let mut registry = FunctionRegistry::default(); + registry.register(foo).unwrap(); + + let function = registry.get(std::any::type_name_of_val(&foo)).unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_register_anonymous_function() { + let mut registry = FunctionRegistry::default(); + registry.register_with_name("foo", || 123_i32).unwrap(); + + let function = registry.get("foo").unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_register_closure() { + let value = 123; + let foo = move || -> i32 { value }; + + let mut registry = FunctionRegistry::default(); + registry.register_with_name("foo", foo).unwrap(); + + let function = registry.get("foo").unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_register_dynamic_function() { + fn foo() -> i32 { + 123 + } + + let function = foo.into_function().with_name("custom_name"); + + let mut registry = FunctionRegistry::default(); + registry.register(function).unwrap(); + + let function = registry.get("custom_name").unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_register_dynamic_closure() { + let value = 123; + let foo = move || -> i32 { value }; + + let function = foo.into_function().with_name("custom_name"); + + let mut registry = FunctionRegistry::default(); + registry.register(function).unwrap(); + + let function = registry.get("custom_name").unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_only_register_function_once() { + fn foo() -> i32 { + 123 + } + + fn bar() -> i32 { + 321 + } + + let name = std::any::type_name_of_val(&foo); + + let mut registry = FunctionRegistry::default(); + registry.register(foo).unwrap(); + let result = registry.register(bar.into_function().with_name(name)); + + assert!(matches!( + result, + Err(FunctionRegistrationError::DuplicateName(_)) + )); + assert_eq!(registry.len(), 1); + + let function = registry.get(name).unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&123)); + } + + #[test] + fn should_allow_overwriting_registration() { + fn foo() -> i32 { + 123 + } + + fn bar() -> i32 { + 321 + } + + let name = std::any::type_name_of_val(&foo); + + let mut registry = FunctionRegistry::default(); + registry.register(foo).unwrap(); + registry + .overwrite_registration(bar.into_function().with_name(name)) + .unwrap(); + + assert_eq!(registry.len(), 1); + + let function = registry.get(name).unwrap(); + let value = function.call(ArgList::new()).unwrap().unwrap_owned(); + assert_eq!(value.try_downcast_ref::(), Some(&321)); + } + + #[test] + fn should_error_on_missing_name() { + let foo = || -> i32 { 123 }; + + let function = foo.into_function(); + + let mut registry = FunctionRegistry::default(); + let result = registry.register(function); + + assert!(matches!( + result, + Err(FunctionRegistrationError::MissingName) + )); + } + + #[test] + fn should_debug_function_registry() { + fn foo() -> i32 { + 123 + } + + let mut registry = FunctionRegistry::default(); + registry.register_with_name("foo", foo).unwrap(); + + let debug = format!("{:?}", registry); + assert_eq!(debug, "{DynamicFunction(fn foo() -> i32)}"); + } +} diff --git a/crates/bevy_reflect/src/func/return_type.rs b/crates/bevy_reflect/src/func/return_type.rs index b9e94dca635ae4..9ad44018d93b23 100644 --- a/crates/bevy_reflect/src/func/return_type.rs +++ b/crates/bevy_reflect/src/func/return_type.rs @@ -1,18 +1,19 @@ -use crate::Reflect; +use crate::PartialReflect; -/// The return type of a [`DynamicFunction`]. +/// The return type of a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug)] pub enum Return<'a> { /// The function returns nothing (i.e. it returns `()`). Unit, /// The function returns an owned value. - Owned(Box), + Owned(Box), /// The function returns a reference to a value. - Ref(&'a dyn Reflect), + Ref(&'a dyn PartialReflect), /// The function returns a mutable reference to a value. - Mut(&'a mut dyn Reflect), + Mut(&'a mut dyn PartialReflect), } impl<'a> Return<'a> { @@ -26,7 +27,7 @@ impl<'a> Return<'a> { /// # Panics /// /// Panics if the return value is not [`Self::Owned`]. - pub fn unwrap_owned(self) -> Box { + pub fn unwrap_owned(self) -> Box { match self { Return::Owned(value) => value, _ => panic!("expected owned value"), @@ -38,7 +39,7 @@ impl<'a> Return<'a> { /// # Panics /// /// Panics if the return value is not [`Self::Ref`]. - pub fn unwrap_ref(self) -> &'a dyn Reflect { + pub fn unwrap_ref(self) -> &'a dyn PartialReflect { match self { Return::Ref(value) => value, _ => panic!("expected reference value"), @@ -50,7 +51,7 @@ impl<'a> Return<'a> { /// # Panics /// /// Panics if the return value is not [`Self::Mut`]. - pub fn unwrap_mut(self) -> &'a mut dyn Reflect { + pub fn unwrap_mut(self) -> &'a mut dyn PartialReflect { match self { Return::Mut(value) => value, _ => panic!("expected mutable reference value"), @@ -60,15 +61,22 @@ impl<'a> Return<'a> { /// A trait for types that can be converted into a [`Return`] value. /// +/// This trait exists so that types can be automatically converted into a [`Return`] +/// by [`ReflectFn`] and [`ReflectFnMut`]. +/// /// This trait is used instead of a blanket [`Into`] implementation due to coherence issues: /// we can't implement `Into` for both `T` and `&T`/`&mut T`. /// /// This trait is automatically implemented when using the `Reflect` [derive macro]. /// +/// [`ReflectFn`]: crate::func::ReflectFn +/// [`ReflectFnMut`]: crate::func::ReflectFnMut /// [derive macro]: derive@crate::Reflect pub trait IntoReturn { /// Converts [`Self`] into a [`Return`] value. - fn into_return<'a>(self) -> Return<'a>; + fn into_return<'a>(self) -> Return<'a> + where + Self: 'a; } impl IntoReturn for () { @@ -111,7 +119,7 @@ macro_rules! impl_into_return { $($U $(: $U1 $(+ $U2)*)?),* )? { - fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> { + fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { $crate::func::Return::Owned(Box::new(self)) } } @@ -125,7 +133,7 @@ macro_rules! impl_into_return { $($U $(: $U1 $(+ $U2)*)?),* )? { - fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> { + fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { $crate::func::Return::Ref(self) } } @@ -139,7 +147,7 @@ macro_rules! impl_into_return { $($U $(: $U1 $(+ $U2)*)?),* )? { - fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> { + fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { $crate::func::Return::Mut(self) } } diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 00e0cc9544755e..6addd3a9468e7a 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -3,36 +3,35 @@ use smallvec::{Array as SmallArray, SmallVec}; use std::any::Any; -use crate::func::macros::impl_function_traits; use crate::utility::GenericTypeInfoCell; use crate::{ self as bevy_reflect, ApplyError, FromReflect, FromType, GetTypeRegistration, List, ListInfo, - ListIter, Reflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, - TypePath, TypeRegistration, Typed, + ListIter, MaybeTyped, PartialReflect, Reflect, ReflectFromPtr, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypePath, TypeRegistration, Typed, }; impl List for SmallVec where - T::Item: FromReflect + TypePath, + T::Item: FromReflect + MaybeTyped + TypePath, { - fn get(&self, index: usize) -> Option<&dyn Reflect> { + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { if index < SmallVec::len(self) { - Some(&self[index] as &dyn Reflect) + Some(&self[index] as &dyn PartialReflect) } else { None } } - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { if index < SmallVec::len(self) { - Some(&mut self[index] as &mut dyn Reflect) + Some(&mut self[index] as &mut dyn PartialReflect) } else { None } } - fn insert(&mut self, index: usize, value: Box) { - let value = value.take::().unwrap_or_else(|value| { + fn insert(&mut self, index: usize, value: Box) { + let value = value.try_take::().unwrap_or_else(|value| { ::Item::from_reflect(&*value).unwrap_or_else(|| { panic!( "Attempted to insert invalid value of type {}.", @@ -43,12 +42,12 @@ where SmallVec::insert(self, index, value); } - fn remove(&mut self, index: usize) -> Box { + fn remove(&mut self, index: usize) -> Box { Box::new(self.remove(index)) } - fn push(&mut self, value: Box) { - let value = value.take::().unwrap_or_else(|value| { + fn push(&mut self, value: Box) { + let value = value.try_take::().unwrap_or_else(|value| { ::Item::from_reflect(&*value).unwrap_or_else(|| { panic!( "Attempted to push invalid value of type {}.", @@ -59,8 +58,9 @@ where SmallVec::push(self, value); } - fn pop(&mut self) -> Option> { - self.pop().map(|value| Box::new(value) as Box) + fn pop(&mut self) -> Option> { + self.pop() + .map(|value| Box::new(value) as Box) } fn len(&self) -> usize { @@ -71,58 +71,53 @@ where ListIter::new(self) } - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { self.into_iter() - .map(|value| Box::new(value) as Box) + .map(|value| Box::new(value) as Box) .collect() } } - -impl Reflect for SmallVec +impl PartialReflect for SmallVec where - T::Item: FromReflect + TypePath, + T::Item: FromReflect + MaybeTyped + TypePath, { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } - fn apply(&mut self, value: &dyn Reflect) { + fn apply(&mut self, value: &dyn PartialReflect) { crate::list_apply(self, value); } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { crate::list_try_apply(self, value) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - fn reflect_kind(&self) -> ReflectKind { ReflectKind::List } @@ -139,18 +134,52 @@ where ReflectOwned::List(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { crate::list_partial_eq(self, value) } } +impl Reflect for SmallVec +where + T::Item: FromReflect + MaybeTyped + TypePath, +{ + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } +} + impl Typed for SmallVec where - T::Item: FromReflect + TypePath, + T::Item: FromReflect + MaybeTyped + TypePath, { fn type_info() -> &'static TypeInfo { static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); @@ -162,9 +191,9 @@ impl_type_path!(::smallvec::SmallVec); impl FromReflect for SmallVec where - T::Item: FromReflect + TypePath, + T::Item: FromReflect + MaybeTyped + TypePath, { - fn from_reflect(reflect: &dyn Reflect) -> Option { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::List(ref_list) = reflect.reflect_ref() { let mut new_list = Self::with_capacity(ref_list.len()); for field in ref_list.iter() { @@ -179,7 +208,7 @@ where impl GetTypeRegistration for SmallVec where - T::Item: FromReflect + TypePath, + T::Item: FromReflect + MaybeTyped + TypePath, { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::>(); @@ -188,4 +217,5 @@ where } } -impl_function_traits!(SmallVec; where T::Item: FromReflect + TypePath); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(SmallVec; where T::Item: FromReflect + MaybeTyped + TypePath); diff --git a/crates/bevy_reflect/src/impls/smol_str.rs b/crates/bevy_reflect/src/impls/smol_str.rs index 0375c1544f2383..d8e3251c91d357 100644 --- a/crates/bevy_reflect/src/impls/smol_str.rs +++ b/crates/bevy_reflect/src/impls/smol_str.rs @@ -6,14 +6,14 @@ impl_reflect_value!(::smol_str::SmolStr(Debug, Hash, PartialEq, Default)); #[cfg(test)] mod tests { - use crate::{FromReflect, Reflect}; + use crate::{FromReflect, PartialReflect}; use smol_str::SmolStr; #[test] fn should_partial_eq_smolstr() { - let a: &dyn Reflect = &SmolStr::new("A"); - let a2: &dyn Reflect = &SmolStr::new("A"); - let b: &dyn Reflect = &SmolStr::new("B"); + let a: &dyn PartialReflect = &SmolStr::new("A"); + let a2: &dyn PartialReflect = &SmolStr::new("A"); + let b: &dyn PartialReflect = &SmolStr::new("B"); assert_eq!(Some(true), a.reflect_partial_eq(a2)); assert_eq!(Some(false), a.reflect_partial_eq(b)); } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 4c1a8284055a61..6320090e6d4f67 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1,15 +1,18 @@ -use crate::func::macros::impl_function_traits; +// Temporary workaround for impl_reflect!(Option/Result false-positive +#![allow(unused_qualifications)] + use crate::std_traits::ReflectDefault; use crate::utility::{ reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell, }; use crate::{ - self as bevy_reflect, impl_type_path, map_apply, map_partial_eq, map_try_apply, ApplyError, - Array, ArrayInfo, ArrayIter, DynamicMap, DynamicTypePath, FromReflect, FromType, - GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, Reflect, - ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, ReflectKind, ReflectMut, ReflectOwned, - ReflectRef, ReflectSerialize, TypeInfo, TypePath, TypeRegistration, TypeRegistry, Typed, - ValueInfo, + self as bevy_reflect, impl_type_path, map_apply, map_partial_eq, map_try_apply, + reflect::impl_full_reflect, set_apply, set_partial_eq, set_try_apply, ApplyError, Array, + ArrayInfo, ArrayIter, DynamicMap, DynamicSet, DynamicTypePath, FromReflect, FromType, + GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, MaybeTyped, + PartialReflect, Reflect, ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, ReflectKind, + ReflectMut, ReflectOwned, ReflectRef, ReflectSerialize, Set, SetInfo, TypeInfo, TypePath, + TypeRegistration, TypeRegistry, Typed, ValueInfo, }; use bevy_reflect_derive::{impl_reflect, impl_reflect_value}; use std::fmt; @@ -98,14 +101,13 @@ impl_reflect_value!(::std::path::PathBuf( )); impl_reflect_value!(::std::any::TypeId(Debug, Hash, PartialEq,)); impl_reflect_value!(::std::collections::BTreeSet()); -impl_reflect_value!(::std::collections::HashSet()); -impl_reflect_value!(::bevy_utils::hashbrown::HashSet()); impl_reflect_value!(::core::ops::Range()); impl_reflect_value!(::core::ops::RangeInclusive()); impl_reflect_value!(::core::ops::RangeFrom()); impl_reflect_value!(::core::ops::RangeTo()); impl_reflect_value!(::core::ops::RangeToInclusive()); impl_reflect_value!(::core::ops::RangeFull()); +impl_reflect_value!(::std::ops::Bound()); impl_reflect_value!(::bevy_utils::Duration( Debug, Hash, @@ -217,25 +219,192 @@ impl_reflect_value!(::std::ffi::OsString( impl_reflect_value!(::std::ffi::OsString(Debug, Hash, PartialEq)); impl_reflect_value!(::alloc::collections::BinaryHeap); -impl_type_path!(::bevy_utils::NoOpHash); -impl_type_path!(::bevy_utils::EntityHash); -impl_type_path!(::bevy_utils::FixedState); +macro_rules! impl_reflect_for_atomic { + ($ty:ty, $ordering:expr) => { + const _: () = { + impl_type_path!($ty); + + #[cfg(feature = "functions")] + crate::func::macros::impl_function_traits!($ty); + + #[allow(unused_mut)] + impl GetTypeRegistration for $ty + where + $ty: Any + Send + Sync, + { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); + registration + } + } + + impl Typed for $ty + where + $ty: Any + Send + Sync, + { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| { + let info = ValueInfo::new::(); + TypeInfo::Value(info) + }) + } + } + + impl PartialReflect for $ty + where + $ty: Any + Send + Sync, + { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + #[inline] + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + #[inline] + fn try_into_reflect( + self: Box, + ) -> Result, Box> { + Ok(self) + } + #[inline] + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + #[inline] + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + #[inline] + fn clone_value(&self) -> Box { + Box::new(<$ty>::new(self.load($ordering))) + } + #[inline] + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + *self = <$ty>::new(value.load($ordering)); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: Into::into(DynamicTypePath::reflect_type_path(value)), + to_type: Into::into(::type_path()), + }); + } + Ok(()) + } + #[inline] + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Value + } + #[inline] + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Value(self) + } + #[inline] + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Value(self) + } + #[inline] + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Value(self) + } + fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self, f) + } + } + + // strange rustfmt bug gives this invocation the wrong indentation! + #[rustfmt::skip] + impl_full_reflect!(for $ty where $ty: Any + Send + Sync); + + impl FromReflect for $ty + where + $ty: Any + Send + Sync, + { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(<$ty>::new( + reflect.try_downcast_ref::<$ty>()?.load($ordering), + )) + } + } + }; + }; +} + +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicIsize, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicUsize, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicI64, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicU64, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicI32, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicU32, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicI16, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicU16, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicI8, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicU8, + ::std::sync::atomic::Ordering::SeqCst +); +impl_reflect_for_atomic!( + ::std::sync::atomic::AtomicBool, + ::std::sync::atomic::Ordering::SeqCst +); macro_rules! impl_reflect_for_veclike { - ($ty:path, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { - impl List for $ty { + ($ty:ty, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { + impl List for $ty { #[inline] - fn get(&self, index: usize) -> Option<&dyn Reflect> { - <$sub>::get(self, index).map(|value| value as &dyn Reflect) + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + <$sub>::get(self, index).map(|value| value as &dyn PartialReflect) } #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { - <$sub>::get_mut(self, index).map(|value| value as &mut dyn Reflect) + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + <$sub>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) } - fn insert(&mut self, index: usize, value: Box) { - let value = value.take::().unwrap_or_else(|value| { + fn insert(&mut self, index: usize, value: Box) { + let value = value.try_take::().unwrap_or_else(|value| { T::from_reflect(&*value).unwrap_or_else(|| { panic!( "Attempted to insert invalid value of type {}.", @@ -246,11 +415,11 @@ macro_rules! impl_reflect_for_veclike { $insert(self, index, value); } - fn remove(&mut self, index: usize) -> Box { + fn remove(&mut self, index: usize) -> Box { Box::new($remove(self, index)) } - fn push(&mut self, value: Box) { + fn push(&mut self, value: Box) { let value = T::take_from_reflect(value).unwrap_or_else(|value| { panic!( "Attempted to push invalid value of type {}.", @@ -260,8 +429,8 @@ macro_rules! impl_reflect_for_veclike { $push(self, value); } - fn pop(&mut self) -> Option> { - $pop(self).map(|value| Box::new(value) as Box) + fn pop(&mut self) -> Option> { + $pop(self).map(|value| Box::new(value) as Box) } #[inline] @@ -275,53 +444,42 @@ macro_rules! impl_reflect_for_veclike { } #[inline] - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { self.into_iter() - .map(|value| Box::new(value) as Box) + .map(|value| Box::new(value) as Box) .collect() } } - impl Reflect for $ty { + impl PartialReflect for $ty { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - - fn into_reflect(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn apply(&mut self, value: &dyn Reflect) { - crate::list_apply(self, value); + fn try_into_reflect( + self: Box, + ) -> Result, Box> { + Ok(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - crate::list_try_apply(self, value) + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -340,7 +498,7 @@ macro_rules! impl_reflect_for_veclike { ReflectOwned::List(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } @@ -348,21 +506,31 @@ macro_rules! impl_reflect_for_veclike { crate::list_hash(self) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { crate::list_partial_eq(self, value) } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::list_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } } - impl Typed for $ty { + impl_full_reflect!( for $ty where T: FromReflect + MaybeTyped + TypePath + GetTypeRegistration); + + impl Typed for $ty { fn type_info() -> &'static TypeInfo { static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); CELL.get_or_insert::(|| TypeInfo::List(ListInfo::new::())) } } - impl_type_path!($ty); - - impl GetTypeRegistration for $ty { + impl GetTypeRegistration + for $ty + { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::<$ty>(); registration.insert::(FromType::<$ty>::from_type()); @@ -374,8 +542,8 @@ macro_rules! impl_reflect_for_veclike { } } - impl FromReflect for $ty { - fn from_reflect(reflect: &dyn Reflect) -> Option { + impl FromReflect for $ty { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::List(ref_list) = reflect.reflect_ref() { let mut new_list = Self::with_capacity(ref_list.len()); for field in ref_list.iter() { @@ -390,56 +558,56 @@ macro_rules! impl_reflect_for_veclike { }; } -impl_reflect_for_veclike!( - ::alloc::vec::Vec, - Vec::insert, - Vec::remove, - Vec::push, - Vec::pop, - [T] -); -impl_function_traits!(Vec; ); +impl_reflect_for_veclike!(Vec, Vec::insert, Vec::remove, Vec::push, Vec::pop, [T]); +impl_type_path!(::alloc::vec::Vec); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Vec; ); impl_reflect_for_veclike!( - ::alloc::collections::VecDeque, + VecDeque, VecDeque::insert, VecDeque::remove, VecDeque::push_back, VecDeque::pop_back, VecDeque:: ); -impl_function_traits!(VecDeque; ); +impl_type_path!(::alloc::collections::VecDeque); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(VecDeque; ); macro_rules! impl_reflect_for_hashmap { ($ty:path) => { impl Map for $ty where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Send + Sync, { - fn get(&self, key: &dyn Reflect) -> Option<&dyn Reflect> { - key.downcast_ref::() + fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { + key.try_downcast_ref::() .and_then(|key| Self::get(self, key)) - .map(|value| value as &dyn Reflect) + .map(|value| value as &dyn PartialReflect) } - fn get_mut(&mut self, key: &dyn Reflect) -> Option<&mut dyn Reflect> { - key.downcast_ref::() + fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { + key.try_downcast_ref::() .and_then(move |key| Self::get_mut(self, key)) - .map(|value| value as &mut dyn Reflect) + .map(|value| value as &mut dyn PartialReflect) } - fn get_at(&self, index: usize) -> Option<(&dyn Reflect, &dyn Reflect)> { + fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { self.iter() .nth(index) - .map(|(key, value)| (key as &dyn Reflect, value as &dyn Reflect)) + .map(|(key, value)| (key as &dyn PartialReflect, value as &dyn PartialReflect)) } - fn get_at_mut(&mut self, index: usize) -> Option<(&dyn Reflect, &mut dyn Reflect)> { - self.iter_mut() - .nth(index) - .map(|(key, value)| (key as &dyn Reflect, value as &mut dyn Reflect)) + fn get_at_mut( + &mut self, + index: usize, + ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { + self.iter_mut().nth(index).map(|(key, value)| { + (key as &dyn PartialReflect, value as &mut dyn PartialReflect) + }) } fn len(&self) -> usize { @@ -450,12 +618,12 @@ macro_rules! impl_reflect_for_hashmap { MapIter::new(self) } - fn drain(self: Box) -> Vec<(Box, Box)> { + fn drain(self: Box) -> Vec<(Box, Box)> { self.into_iter() .map(|(key, value)| { ( - Box::new(key) as Box, - Box::new(value) as Box, + Box::new(key) as Box, + Box::new(value) as Box, ) }) .collect() @@ -478,9 +646,9 @@ macro_rules! impl_reflect_for_hashmap { fn insert_boxed( &mut self, - key: Box, - value: Box, - ) -> Option> { + key: Box, + value: Box, + ) -> Option> { let key = K::take_from_reflect(key).unwrap_or_else(|key| { panic!( "Attempted to insert invalid key of type {}.", @@ -494,67 +662,56 @@ macro_rules! impl_reflect_for_hashmap { ) }); self.insert(key, value) - .map(|old_value| Box::new(old_value) as Box) + .map(|old_value| Box::new(old_value) as Box) } - fn remove(&mut self, key: &dyn Reflect) -> Option> { + fn remove(&mut self, key: &dyn PartialReflect) -> Option> { let mut from_reflect = None; - key.downcast_ref::() + key.try_downcast_ref::() .or_else(|| { from_reflect = K::from_reflect(key); from_reflect.as_ref() }) .and_then(|key| self.remove(key)) - .map(|value| Box::new(value) as Box) + .map(|value| Box::new(value) as Box) } } - impl Reflect for $ty + impl PartialReflect for $ty where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Send + Sync, { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - #[inline] - fn into_reflect(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn apply(&mut self, value: &dyn Reflect) { - map_apply(self, value); + fn try_into_reflect( + self: Box, + ) -> Result, Box> { + Ok(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - map_try_apply(self, value) + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -573,19 +730,35 @@ macro_rules! impl_reflect_for_hashmap { ReflectOwned::Map(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { map_partial_eq(self, value) } + + fn apply(&mut self, value: &dyn PartialReflect) { + map_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } } + impl_full_reflect!( + for $ty + where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Send + Sync, + ); + impl Typed for $ty where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Send + Sync, { fn type_info() -> &'static TypeInfo { @@ -596,8 +769,8 @@ macro_rules! impl_reflect_for_hashmap { impl GetTypeRegistration for $ty where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Send + Sync, { fn get_type_registration() -> TypeRegistration { @@ -614,11 +787,11 @@ macro_rules! impl_reflect_for_hashmap { impl FromReflect for $ty where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Default + Send + Sync, { - fn from_reflect(reflect: &dyn Reflect) -> Option { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::Map(ref_map) = reflect.reflect_ref() { let mut new_map = Self::with_capacity_and_hasher(ref_map.len(), S::default()); for (key, value) in ref_map.iter() { @@ -638,10 +811,11 @@ macro_rules! impl_reflect_for_hashmap { impl_reflect_for_hashmap!(::std::collections::HashMap); impl_type_path!(::std::collections::hash_map::RandomState); impl_type_path!(::std::collections::HashMap); -impl_function_traits!(::std::collections::HashMap; +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::HashMap; < - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Default + Send + Sync > ); @@ -649,41 +823,265 @@ impl_function_traits!(::std::collections::HashMap; impl_reflect_for_hashmap!(bevy_utils::hashbrown::HashMap); impl_type_path!(::bevy_utils::hashbrown::hash_map::DefaultHashBuilder); impl_type_path!(::bevy_utils::hashbrown::HashMap); -impl_function_traits!(::bevy_utils::hashbrown::HashMap; +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::bevy_utils::hashbrown::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +macro_rules! impl_reflect_for_hashset { + ($ty:path) => { + impl Set for $ty + where + V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, + S: TypePath + BuildHasher + Send + Sync, + { + fn get(&self, value: &dyn PartialReflect) -> Option<&dyn PartialReflect> { + value + .try_downcast_ref::() + .and_then(|value| Self::get(self, value)) + .map(|value| value as &dyn PartialReflect) + } + + fn len(&self) -> usize { + Self::len(self) + } + + fn iter(&self) -> Box + '_> { + let iter = self.iter().map(|v| v as &dyn PartialReflect); + Box::new(iter) + } + + fn drain(self: Box) -> Vec> { + self.into_iter() + .map(|value| Box::new(value) as Box) + .collect() + } + + fn clone_dynamic(&self) -> DynamicSet { + let mut dynamic_set = DynamicSet::default(); + dynamic_set.set_represented_type(self.get_represented_type_info()); + for v in self { + dynamic_set.insert_boxed(v.clone_value()); + } + dynamic_set + } + + fn insert_boxed(&mut self, value: Box) -> bool { + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ) + }); + self.insert(value) + } + + fn remove(&mut self, value: &dyn PartialReflect) -> bool { + let mut from_reflect = None; + value + .try_downcast_ref::() + .or_else(|| { + from_reflect = V::from_reflect(value); + from_reflect.as_ref() + }) + .map_or(false, |value| self.remove(value)) + } + + fn contains(&self, value: &dyn PartialReflect) -> bool { + let mut from_reflect = None; + value + .try_downcast_ref::() + .or_else(|| { + from_reflect = V::from_reflect(value); + from_reflect.as_ref() + }) + .map_or(false, |value| self.contains(value)) + } + } + + impl PartialReflect for $ty + where + V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, + S: TypePath + BuildHasher + Send + Sync, + { + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + Some(::type_info()) + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + #[inline] + fn try_into_reflect( + self: Box, + ) -> Result, Box> { + Ok(self) + } + + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) + } + + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) + } + + fn apply(&mut self, value: &dyn PartialReflect) { + set_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + set_try_apply(self, value) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Set + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Set(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Set(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Set(self) + } + + fn clone_value(&self) -> Box { + Box::new(self.clone_dynamic()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + set_partial_eq(self, value) + } + } + + impl Typed for $ty + where + V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, + S: TypePath + BuildHasher + Send + Sync, + { + fn type_info() -> &'static TypeInfo { + static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); + CELL.get_or_insert::(|| TypeInfo::Set(SetInfo::new::())) + } + } + + impl GetTypeRegistration for $ty + where + V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, + S: TypePath + BuildHasher + Send + Sync, + { + fn get_type_registration() -> TypeRegistration { + let mut registration = TypeRegistration::of::(); + registration.insert::(FromType::::from_type()); + registration + } + + fn register_type_dependencies(registry: &mut TypeRegistry) { + registry.register::(); + } + } + + impl_full_reflect!( + for $ty + where + V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, + S: TypePath + BuildHasher + Send + Sync, + ); + + impl FromReflect for $ty + where + V: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, + S: TypePath + BuildHasher + Default + Send + Sync, + { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + if let ReflectRef::Set(ref_set) = reflect.reflect_ref() { + let mut new_set = Self::with_capacity_and_hasher(ref_set.len(), S::default()); + for value in ref_set.iter() { + let new_value = V::from_reflect(value)?; + new_set.insert(new_value); + } + Some(new_set) + } else { + None + } + } + } + }; +} + +impl_type_path!(::bevy_utils::NoOpHash); +impl_type_path!(::bevy_utils::EntityHash); +impl_type_path!(::bevy_utils::FixedState); + +impl_reflect_for_hashset!(::std::collections::HashSet); +impl_type_path!(::std::collections::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::HashSet; < - K: FromReflect + TypePath + GetTypeRegistration + Eq + Hash, - V: FromReflect + TypePath + GetTypeRegistration, + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + +impl_reflect_for_hashset!(::bevy_utils::hashbrown::HashSet); +impl_type_path!(::bevy_utils::hashbrown::HashSet); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::bevy_utils::hashbrown::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, S: TypePath + BuildHasher + Default + Send + Sync > ); impl Map for ::std::collections::BTreeMap where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, { - fn get(&self, key: &dyn Reflect) -> Option<&dyn Reflect> { - key.downcast_ref::() + fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { + key.try_downcast_ref::() .and_then(|key| Self::get(self, key)) - .map(|value| value as &dyn Reflect) + .map(|value| value as &dyn PartialReflect) } - fn get_mut(&mut self, key: &dyn Reflect) -> Option<&mut dyn Reflect> { - key.downcast_ref::() + fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { + key.try_downcast_ref::() .and_then(move |key| Self::get_mut(self, key)) - .map(|value| value as &mut dyn Reflect) + .map(|value| value as &mut dyn PartialReflect) } - fn get_at(&self, index: usize) -> Option<(&dyn Reflect, &dyn Reflect)> { + fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { self.iter() .nth(index) - .map(|(key, value)| (key as &dyn Reflect, value as &dyn Reflect)) + .map(|(key, value)| (key as &dyn PartialReflect, value as &dyn PartialReflect)) } - fn get_at_mut(&mut self, index: usize) -> Option<(&dyn Reflect, &mut dyn Reflect)> { + fn get_at_mut( + &mut self, + index: usize, + ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { self.iter_mut() .nth(index) - .map(|(key, value)| (key as &dyn Reflect, value as &mut dyn Reflect)) + .map(|(key, value)| (key as &dyn PartialReflect, value as &mut dyn PartialReflect)) } fn len(&self) -> usize { @@ -694,12 +1092,12 @@ where MapIter::new(self) } - fn drain(self: Box) -> Vec<(Box, Box)> { + fn drain(self: Box) -> Vec<(Box, Box)> { self.into_iter() .map(|(key, value)| { ( - Box::new(key) as Box, - Box::new(value) as Box, + Box::new(key) as Box, + Box::new(value) as Box, ) }) .collect() @@ -722,9 +1120,9 @@ where fn insert_boxed( &mut self, - key: Box, - value: Box, - ) -> Option> { + key: Box, + value: Box, + ) -> Option> { let key = K::take_from_reflect(key).unwrap_or_else(|key| { panic!( "Attempted to insert invalid key of type {}.", @@ -738,68 +1136,53 @@ where ) }); self.insert(key, value) - .map(|old_value| Box::new(old_value) as Box) + .map(|old_value| Box::new(old_value) as Box) } - fn remove(&mut self, key: &dyn Reflect) -> Option> { + fn remove(&mut self, key: &dyn PartialReflect) -> Option> { let mut from_reflect = None; - key.downcast_ref::() + key.try_downcast_ref::() .or_else(|| { from_reflect = K::from_reflect(key); from_reflect.as_ref() }) .and_then(|key| self.remove(key)) - .map(|value| Box::new(value) as Box) + .map(|value| Box::new(value) as Box) } } -impl Reflect for ::std::collections::BTreeMap +impl PartialReflect for ::std::collections::BTreeMap where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - #[inline] - fn into_reflect(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - - fn apply(&mut self, value: &dyn Reflect) { - map_apply(self, value); + #[inline] + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - map_try_apply(self, value) + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } - fn reflect_kind(&self) -> ReflectKind { ReflectKind::Map } @@ -816,19 +1199,34 @@ where ReflectOwned::Map(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { map_partial_eq(self, value) } -} -impl Typed for ::std::collections::BTreeMap + fn apply(&mut self, value: &dyn PartialReflect) { + map_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } +} + +impl_full_reflect!( + for ::std::collections::BTreeMap + where + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, +); + +impl Typed for ::std::collections::BTreeMap where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, { fn type_info() -> &'static TypeInfo { static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); @@ -838,8 +1236,8 @@ where impl GetTypeRegistration for ::std::collections::BTreeMap where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::(); @@ -850,10 +1248,10 @@ where impl FromReflect for ::std::collections::BTreeMap where - K: FromReflect + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + TypePath + GetTypeRegistration, + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, { - fn from_reflect(reflect: &dyn Reflect) -> Option { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::Map(ref_map) = reflect.reflect_ref() { let mut new_map = Self::new(); for (key, value) in ref_map.iter() { @@ -869,22 +1267,23 @@ where } impl_type_path!(::std::collections::BTreeMap); -impl_function_traits!(::std::collections::BTreeMap; +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(::std::collections::BTreeMap; < - K: FromReflect + TypePath + GetTypeRegistration + Eq + Ord, - V: FromReflect + TypePath + GetTypeRegistration + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration > ); -impl Array for [T; N] { +impl Array for [T; N] { #[inline] - fn get(&self, index: usize) -> Option<&dyn Reflect> { - <[T]>::get(self, index).map(|value| value as &dyn Reflect) + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + <[T]>::get(self, index).map(|value| value as &dyn PartialReflect) } #[inline] - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { - <[T]>::get_mut(self, index).map(|value| value as &mut dyn Reflect) + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + <[T]>::get_mut(self, index).map(|value| value as &mut dyn PartialReflect) } #[inline] @@ -898,62 +1297,43 @@ impl Array for [T; } #[inline] - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { self.into_iter() - .map(|value| Box::new(value) as Box) + .map(|value| Box::new(value) as Box) .collect() } } -impl Reflect for [T; N] { +impl PartialReflect + for [T; N] +{ fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } - #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self - } - - #[inline] - fn apply(&mut self, value: &dyn Reflect) { - crate::array_apply(self, value); - } - - #[inline] - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - crate::array_try_apply(self, value) - } - - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } #[inline] @@ -977,7 +1357,7 @@ impl Reflect for [T } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } @@ -987,13 +1367,62 @@ impl Reflect for [T } #[inline] - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { crate::array_partial_eq(self, value) } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::array_apply(self, value); + } + + #[inline] + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::array_try_apply(self, value) + } +} + +impl Reflect for [T; N] { + #[inline] + fn into_any(self: Box) -> Box { + self + } + + #[inline] + fn as_any(&self) -> &dyn Any { + self + } + + #[inline] + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + #[inline] + fn into_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_reflect(&self) -> &dyn Reflect { + self + } + + #[inline] + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + #[inline] + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } } -impl FromReflect for [T; N] { - fn from_reflect(reflect: &dyn Reflect) -> Option { +impl FromReflect + for [T; N] +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::Array(ref_array) = reflect.reflect_ref() { let mut temp_vec = Vec::with_capacity(ref_array.len()); for field in ref_array.iter() { @@ -1006,7 +1435,7 @@ impl FromReflec } } -impl Typed for [T; N] { +impl Typed for [T; N] { fn type_info() -> &'static TypeInfo { static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); CELL.get_or_insert::(|| TypeInfo::Array(ArrayInfo::new::(N))) @@ -1025,7 +1454,9 @@ impl TypePath for [T; N] { } } -impl GetTypeRegistration for [T; N] { +impl GetTypeRegistration + for [T; N] +{ fn get_type_registration() -> TypeRegistration { TypeRegistration::of::<[T; N]>() } @@ -1035,7 +1466,8 @@ impl GetTypeRegistr } } -impl_function_traits!([T; N]; [const N: usize]); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!([T; N]; [const N: usize]); impl_reflect! { #[type_path = "core::option"] @@ -1077,52 +1509,34 @@ impl TypePath for &'static mut T { } } -impl Reflect for Cow<'static, str> { +impl PartialReflect for Cow<'static, str> { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn into_reflect(self: Box) -> Box { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - let any = value.as_any(); - if let Some(value) = any.downcast_ref::() { - self.clone_from(value); - } else { - return Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static - to_type: Self::type_path().into(), - }); - } - Ok(()) + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -1141,21 +1555,20 @@ impl Reflect for Cow<'static, str> { ReflectOwned::Value(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone()) } fn reflect_hash(&self) -> Option { let mut hasher = reflect_hasher(); - Hash::hash(&std::any::Any::type_id(self), &mut hasher); + Hash::hash(&Any::type_id(self), &mut hasher); Hash::hash(self, &mut hasher); Some(hasher.finish()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { - Some(std::cmp::PartialEq::eq(self, value)) + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) } else { Some(false) } @@ -1164,8 +1577,23 @@ impl Reflect for Cow<'static, str> { fn debug(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { fmt::Debug::fmt(self, f) } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static + to_type: Self::type_path().into(), + }); + } + Ok(()) + } } +impl_full_reflect!(for Cow<'static, str>); + impl Typed for Cow<'static, str> { fn type_info() -> &'static TypeInfo { static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); @@ -1184,17 +1612,13 @@ impl GetTypeRegistration for Cow<'static, str> { } impl FromReflect for Cow<'static, str> { - fn from_reflect(reflect: &dyn crate::Reflect) -> Option { - Some( - reflect - .as_any() - .downcast_ref::>()? - .clone(), - ) + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::>()?.clone()) } } -impl_function_traits!(Cow<'static, str>); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, str>); impl TypePath for [T] where @@ -1211,32 +1635,34 @@ where } } -impl List for Cow<'static, [T]> { - fn get(&self, index: usize) -> Option<&dyn Reflect> { - self.as_ref().get(index).map(|x| x as &dyn Reflect) +impl List + for Cow<'static, [T]> +{ + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { + self.as_ref().get(index).map(|x| x as &dyn PartialReflect) } - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { - self.to_mut().get_mut(index).map(|x| x as &mut dyn Reflect) + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + self.to_mut() + .get_mut(index) + .map(|x| x as &mut dyn PartialReflect) } - fn insert(&mut self, index: usize, element: Box) { - let value = element.take::().unwrap_or_else(|value| { - T::from_reflect(&*value).unwrap_or_else(|| { - panic!( - "Attempted to insert invalid value of type {}.", - value.reflect_type_path() - ) - }) + fn insert(&mut self, index: usize, element: Box) { + let value = T::take_from_reflect(element).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.reflect_type_path() + ); }); self.to_mut().insert(index, value); } - fn remove(&mut self, index: usize) -> Box { + fn remove(&mut self, index: usize) -> Box { Box::new(self.to_mut().remove(index)) } - fn push(&mut self, value: Box) { + fn push(&mut self, value: Box) { let value = T::take_from_reflect(value).unwrap_or_else(|value| { panic!( "Attempted to push invalid value of type {}.", @@ -1246,10 +1672,10 @@ impl List for Cow<'stat self.to_mut().push(value); } - fn pop(&mut self) -> Option> { + fn pop(&mut self) -> Option> { self.to_mut() .pop() - .map(|value| Box::new(value) as Box) + .map(|value| Box::new(value) as Box) } fn len(&self) -> usize { @@ -1260,7 +1686,7 @@ impl List for Cow<'stat ListIter::new(self) } - fn drain(self: Box) -> Vec> { + 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() @@ -1270,46 +1696,36 @@ impl List for Cow<'stat } } -impl Reflect for Cow<'static, [T]> { +impl PartialReflect + for Cow<'static, [T]> +{ fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn into_reflect(self: Box) -> Box { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn apply(&mut self, value: &dyn Reflect) { - crate::list_apply(self, value); + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - crate::list_try_apply(self, value) - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -1328,7 +1744,7 @@ impl Reflect for Cow<'s ReflectOwned::List(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(List::clone_dynamic(self)) } @@ -1336,19 +1752,35 @@ impl Reflect for Cow<'s crate::list_hash(self) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { crate::list_partial_eq(self, value) } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::list_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } } -impl Typed for Cow<'static, [T]> { +impl_full_reflect!( + for Cow<'static, [T]> + where + T: FromReflect + Clone + MaybeTyped + TypePath + GetTypeRegistration, +); + +impl Typed + for Cow<'static, [T]> +{ fn type_info() -> &'static TypeInfo { static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); CELL.get_or_insert::(|| TypeInfo::List(ListInfo::new::())) } } -impl GetTypeRegistration +impl GetTypeRegistration for Cow<'static, [T]> { fn get_type_registration() -> TypeRegistration { @@ -1360,8 +1792,10 @@ impl GetTypeRegistratio } } -impl FromReflect for Cow<'static, [T]> { - fn from_reflect(reflect: &dyn Reflect) -> Option { +impl FromReflect + for Cow<'static, [T]> +{ + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::List(ref_list) = reflect.reflect_ref() { let mut temp_vec = Vec::with_capacity(ref_list.len()); for field in ref_list.iter() { @@ -1374,53 +1808,37 @@ impl FromReflect for Co } } -impl_function_traits!(Cow<'static, [T]>; ); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, [T]>; ); -impl Reflect for &'static str { +impl PartialReflect for &'static str { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn into_reflect(self: Box) -> Box { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - let any = value.as_any(); - if let Some(&value) = any.downcast_ref::() { - *self = value; - } else { - return Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: Self::type_path().into(), - }); - } - Ok(()) + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_ref(&self) -> ReflectRef { @@ -1435,21 +1853,20 @@ impl Reflect for &'static str { ReflectOwned::Value(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(*self) } fn reflect_hash(&self) -> Option { let mut hasher = reflect_hasher(); - Hash::hash(&std::any::Any::type_id(self), &mut hasher); + Hash::hash(&Any::type_id(self), &mut hasher); Hash::hash(self, &mut hasher); Some(hasher.finish()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { - Some(std::cmp::PartialEq::eq(self, value)) + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) } else { Some(false) } @@ -1458,6 +1875,49 @@ impl Reflect for &'static str { fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self, f) } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + } else { + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: Self::type_path().into(), + }); + } + Ok(()) + } +} + +impl Reflect for &'static str { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } } impl Typed for &'static str { @@ -1477,58 +1937,42 @@ impl GetTypeRegistration for &'static str { } impl FromReflect for &'static str { - fn from_reflect(reflect: &dyn crate::Reflect) -> Option { - reflect.as_any().downcast_ref::().copied() + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() } } -impl_function_traits!(&'static str); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static str); -impl Reflect for &'static Path { +impl PartialReflect for &'static Path { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn into_reflect(self: Box) -> Box { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - let any = value.as_any(); - if let Some(&value) = any.downcast_ref::() { - *self = value; - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -1547,25 +1991,67 @@ impl Reflect for &'static Path { ReflectOwned::Value(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(*self) } fn reflect_hash(&self) -> Option { let mut hasher = reflect_hasher(); - Hash::hash(&std::any::Any::type_id(self), &mut hasher); + Hash::hash(&Any::type_id(self), &mut hasher); Hash::hash(self, &mut hasher); Some(hasher.finish()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { - Some(std::cmp::PartialEq::eq(self, value)) + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) } else { Some(false) } } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for &'static Path { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } } impl Typed for &'static Path { @@ -1584,58 +2070,42 @@ impl GetTypeRegistration for &'static Path { } impl FromReflect for &'static Path { - fn from_reflect(reflect: &dyn crate::Reflect) -> Option { - reflect.as_any().downcast_ref::().copied() + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + reflect.try_downcast_ref::().copied() } } -impl_function_traits!(&'static Path); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(&'static Path); -impl Reflect for Cow<'static, Path> { +impl PartialReflect for Cow<'static, Path> { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn into_reflect(self: Box) -> Box { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - let any = value.as_any(); - if let Some(value) = any.downcast_ref::() { - self.clone_from(value); - Ok(()) - } else { - Err(ApplyError::MismatchedTypes { - from_type: value.reflect_type_path().into(), - to_type: ::reflect_type_path(self).into(), - }) - } - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -1654,21 +2124,20 @@ impl Reflect for Cow<'static, Path> { ReflectOwned::Value(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone()) } fn reflect_hash(&self) -> Option { let mut hasher = reflect_hasher(); - Hash::hash(&std::any::Any::type_id(self), &mut hasher); + Hash::hash(&Any::type_id(self), &mut hasher); Hash::hash(self, &mut hasher); Some(hasher.finish()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { - Some(std::cmp::PartialEq::eq(self, value)) + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + if let Some(value) = value.try_downcast_ref::() { + Some(PartialEq::eq(self, value)) } else { Some(false) } @@ -1677,6 +2146,49 @@ impl Reflect for Cow<'static, Path> { fn debug(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self, f) } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + if let Some(value) = value.try_downcast_ref::() { + self.clone_from(value); + Ok(()) + } else { + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) + } + } +} + +impl Reflect for Cow<'static, Path> { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } } impl Typed for Cow<'static, Path> { @@ -1690,8 +2202,8 @@ impl_type_path!(::std::path::Path); impl_type_path!(::alloc::borrow::Cow<'a: 'static, T: ToOwned + ?Sized>); impl FromReflect for Cow<'static, Path> { - fn from_reflect(reflect: &dyn Reflect) -> Option { - Some(reflect.as_any().downcast_ref::()?.clone()) + fn from_reflect(reflect: &dyn PartialReflect) -> Option { + Some(reflect.try_downcast_ref::()?.clone()) } } @@ -1706,11 +2218,12 @@ impl GetTypeRegistration for Cow<'static, Path> { } } -impl_function_traits!(Cow<'static, Path>); +#[cfg(feature = "functions")] +crate::func::macros::impl_function_traits!(Cow<'static, Path>); #[cfg(test)] mod tests { - use crate as bevy_reflect; + use crate::{self as bevy_reflect, PartialReflect}; use crate::{ Enum, FromReflect, Reflect, ReflectSerialize, TypeInfo, TypeRegistry, Typed, VariantInfo, VariantType, @@ -1735,45 +2248,45 @@ mod tests { #[test] fn should_partial_eq_char() { - let a: &dyn Reflect = &'x'; - let b: &dyn Reflect = &'x'; - let c: &dyn Reflect = &'o'; + let a: &dyn PartialReflect = &'x'; + let b: &dyn PartialReflect = &'x'; + let c: &dyn PartialReflect = &'o'; assert!(a.reflect_partial_eq(b).unwrap_or_default()); assert!(!a.reflect_partial_eq(c).unwrap_or_default()); } #[test] fn should_partial_eq_i32() { - let a: &dyn Reflect = &123_i32; - let b: &dyn Reflect = &123_i32; - let c: &dyn Reflect = &321_i32; + let a: &dyn PartialReflect = &123_i32; + let b: &dyn PartialReflect = &123_i32; + let c: &dyn PartialReflect = &321_i32; assert!(a.reflect_partial_eq(b).unwrap_or_default()); assert!(!a.reflect_partial_eq(c).unwrap_or_default()); } #[test] fn should_partial_eq_f32() { - let a: &dyn Reflect = &PI; - let b: &dyn Reflect = &PI; - let c: &dyn Reflect = &TAU; + let a: &dyn PartialReflect = &PI; + let b: &dyn PartialReflect = &PI; + let c: &dyn PartialReflect = &TAU; assert!(a.reflect_partial_eq(b).unwrap_or_default()); assert!(!a.reflect_partial_eq(c).unwrap_or_default()); } #[test] fn should_partial_eq_string() { - let a: &dyn Reflect = &String::from("Hello"); - let b: &dyn Reflect = &String::from("Hello"); - let c: &dyn Reflect = &String::from("World"); + let a: &dyn PartialReflect = &String::from("Hello"); + let b: &dyn PartialReflect = &String::from("Hello"); + let c: &dyn PartialReflect = &String::from("World"); assert!(a.reflect_partial_eq(b).unwrap_or_default()); assert!(!a.reflect_partial_eq(c).unwrap_or_default()); } #[test] fn should_partial_eq_vec() { - let a: &dyn Reflect = &vec![1, 2, 3]; - let b: &dyn Reflect = &vec![1, 2, 3]; - let c: &dyn Reflect = &vec![3, 2, 1]; + let a: &dyn PartialReflect = &vec![1, 2, 3]; + let b: &dyn PartialReflect = &vec![1, 2, 3]; + let c: &dyn PartialReflect = &vec![3, 2, 1]; assert!(a.reflect_partial_eq(b).unwrap_or_default()); assert!(!a.reflect_partial_eq(c).unwrap_or_default()); } @@ -1786,9 +2299,9 @@ mod tests { let mut c = HashMap::new(); c.insert(0usize, 3.21_f64); - let a: &dyn Reflect = &a; - let b: &dyn Reflect = &b; - let c: &dyn Reflect = &c; + let a: &dyn PartialReflect = &a; + let b: &dyn PartialReflect = &b; + let c: &dyn PartialReflect = &c; assert!(a.reflect_partial_eq(b).unwrap_or_default()); assert!(!a.reflect_partial_eq(c).unwrap_or_default()); } @@ -1804,14 +2317,18 @@ mod tests { let a: &dyn Reflect = &a; let b: &dyn Reflect = &b; let c: &dyn Reflect = &c; - assert!(a.reflect_partial_eq(b).unwrap_or_default()); - assert!(!a.reflect_partial_eq(c).unwrap_or_default()); + assert!(a + .reflect_partial_eq(b.as_partial_reflect()) + .unwrap_or_default()); + assert!(!a + .reflect_partial_eq(c.as_partial_reflect()) + .unwrap_or_default()); } #[test] fn should_partial_eq_option() { - let a: &dyn Reflect = &Some(123); - let b: &dyn Reflect = &Some(123); + let a: &dyn PartialReflect = &Some(123); + let b: &dyn PartialReflect = &Some(123); assert_eq!(Some(true), a.reflect_partial_eq(b)); } @@ -1834,7 +2351,7 @@ mod tests { if value.is_variant(VariantType::Tuple) { if let Some(field) = value .field_at_mut(0) - .and_then(|field| field.downcast_mut::()) + .and_then(|field| field.try_downcast_mut::()) { *field = 321; } @@ -1864,28 +2381,28 @@ mod tests { // === None on None === // let patch = None::; let mut value = None::; - Reflect::apply(&mut value, &patch); + PartialReflect::apply(&mut value, &patch); assert_eq!(patch, value, "None apply onto None"); // === Some on None === // let patch = Some(Foo(123)); let mut value = None::; - Reflect::apply(&mut value, &patch); + PartialReflect::apply(&mut value, &patch); assert_eq!(patch, value, "Some apply onto None"); // === None on Some === // let patch = None::; let mut value = Some(Foo(321)); - Reflect::apply(&mut value, &patch); + PartialReflect::apply(&mut value, &patch); assert_eq!(patch, value, "None apply onto Some"); // === Some on Some === // let patch = Some(Foo(123)); let mut value = Some(Foo(321)); - Reflect::apply(&mut value, &patch); + PartialReflect::apply(&mut value, &patch); assert_eq!(patch, value, "Some apply onto Some"); } @@ -1927,10 +2444,10 @@ mod tests { #[test] fn nonzero_usize_impl_reflect_from_reflect() { - let a: &dyn Reflect = &std::num::NonZeroUsize::new(42).unwrap(); - let b: &dyn Reflect = &std::num::NonZeroUsize::new(42).unwrap(); + let a: &dyn PartialReflect = &std::num::NonZeroUsize::new(42).unwrap(); + let b: &dyn PartialReflect = &std::num::NonZeroUsize::new(42).unwrap(); assert!(a.reflect_partial_eq(b).unwrap_or_default()); - let forty_two: std::num::NonZeroUsize = crate::FromReflect::from_reflect(a).unwrap(); + let forty_two: std::num::NonZeroUsize = FromReflect::from_reflect(a).unwrap(); assert_eq!(forty_two, std::num::NonZeroUsize::new(42).unwrap()); } diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index dcd77bb214aedf..941105a851e992 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1,6 +1,8 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +#![allow(internal_features)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" @@ -26,15 +28,60 @@ //! It's important to note that because of missing features in Rust, //! there are some [limitations] with this crate. //! -//! # The `Reflect` Trait +//! # The `Reflect` and `PartialReflect` traits //! -//! At the core of [`bevy_reflect`] is the [`Reflect`] trait. +//! At the root of [`bevy_reflect`] is the [`PartialReflect`] trait. //! -//! One of its primary purposes is to allow all implementors to be passed around -//! as a `dyn Reflect` trait object. -//! This allows any such type to be operated upon completely dynamically (at a small [runtime cost]). +//! Its purpose is to allow dynamic [introspection] of values, +//! following Rust's type system through a system of [subtraits]. //! -//! Implementing the trait is easily done using the provided [derive macro]: +//! Its primary purpose is to allow all implementors to be passed around +//! as a `dyn PartialReflect` trait object in one of the following forms: +//! * `&dyn PartialReflect` +//! * `&mut dyn PartialReflect` +//! * `Box` +//! +//! This allows values of types implementing `PartialReflect` +//! to be operated upon completely dynamically (at a small [runtime cost]). +//! +//! Building on `PartialReflect` is the [`Reflect`] trait. +//! +//! `PartialReflect` is a supertrait of `Reflect` +//! so any type implementing `Reflect` implements `PartialReflect` by definition. +//! `dyn Reflect` trait objects can be used similarly to `dyn PartialReflect`, +//! but `Reflect` is also often used in trait bounds (like `T: Reflect`). +//! +//! The distinction between `PartialReflect` and `Reflect` is summarised in the following: +//! * `PartialReflect` is a trait for interacting with values under `bevy_reflect`'s data model. +//! This means values implementing `PartialReflect` can be dynamically constructed and introspected. +//! * The `Reflect` trait, however, ensures that the interface exposed by `PartialReflect` +//! on types which additionally implement `Reflect` mirrors the structure of a single Rust type. +//! * This means `dyn Reflect` trait objects can be directly downcasted to concrete types, +//! where `dyn PartialReflect` trait object cannot. +//! * `Reflect`, since it provides a stronger type-correctness guarantee, +//! is the trait used to interact with [the type registry]. +//! +//! ## Converting between `PartialReflect` and `Reflect` +//! +//! Since `T: Reflect` implies `T: PartialReflect`, conversion from a `dyn Reflect` to a `dyn PartialReflect` +//! trait object (upcasting) is infallible and can be performed with one of the following methods. +//! Note that these are temporary while [the language feature for dyn upcasting coercion] is experimental: +//! * [`PartialReflect::as_partial_reflect`] for `&dyn PartialReflect` +//! * [`PartialReflect::as_partial_reflect_mut`] for `&mut dyn PartialReflect` +//! * [`PartialReflect::into_partial_reflect`] for `Box` +//! +//! For conversion in the other direction — downcasting `dyn PartialReflect` to `dyn Reflect` — +//! there are fallible methods: +//! * [`PartialReflect::try_as_reflect`] for `&dyn Reflect` +//! * [`PartialReflect::try_as_reflect_mut`] for `&mut dyn Reflect` +//! * [`PartialReflect::try_into_reflect`] for `Box` +//! +//! Additionally, [`FromReflect::from_reflect`] can be used to convert a `dyn PartialReflect` to a concrete type +//! which implements `Reflect`. +//! +//! # Implementing `Reflect` +//! +//! Implementing `Reflect` (and `PartialReflect`) is easily done using the provided [derive macro]: //! //! ``` //! # use bevy_reflect::Reflect; @@ -54,8 +101,8 @@ //! ## Requirements //! //! We can implement `Reflect` on any type that satisfies _both_ of the following conditions: -//! * The type implements `Any`. -//! This is true if and only if the type itself has a [`'static` lifetime]. +//! * The type implements `Any`, `Send`, and `Sync`. +//! For the `Any` requirement to be satisfied, the type itself must have a [`'static` lifetime]. //! * All fields and sub-elements themselves implement `Reflect` //! (see the [derive macro documentation] for details on how to ignore certain fields when deriving). //! @@ -63,11 +110,11 @@ //! * All fields and sub-elements must implement [`FromReflect`]— //! another important reflection trait discussed in a later section. //! -//! # The `Reflect` Subtraits +//! # The Reflection Subtraits //! -//! Since [`Reflect`] is meant to cover any and every type, this crate also comes with a few -//! more traits to accompany `Reflect` and provide more specific interactions. -//! We refer to these traits as the _reflection subtraits_ since they all have `Reflect` as a supertrait. +//! Since [`PartialReflect`] is meant to cover any and every type, this crate also comes with a few +//! more traits to accompany `PartialReflect` and provide more specific interactions. +//! We refer to these traits as the _reflection subtraits_ since they all have `PartialReflect` as a supertrait. //! The current list of reflection subtraits include: //! * [`Tuple`] //! * [`Array`] @@ -83,7 +130,7 @@ //! For example, we can access our struct's fields by name using the [`Struct::field`] method. //! //! ``` -//! # use bevy_reflect::{Reflect, Struct}; +//! # use bevy_reflect::{PartialReflect, Reflect, Struct}; //! # #[derive(Reflect)] //! # struct MyStruct { //! # foo: i32 @@ -91,28 +138,29 @@ //! let my_struct: Box = Box::new(MyStruct { //! foo: 123 //! }); -//! let foo: &dyn Reflect = my_struct.field("foo").unwrap(); -//! assert_eq!(Some(&123), foo.downcast_ref::()); +//! let foo: &dyn PartialReflect = my_struct.field("foo").unwrap(); +//! assert_eq!(Some(&123), foo.try_downcast_ref::()); //! ``` //! -//! Since most data is passed around as `dyn Reflect`, -//! the `Reflect` trait has methods for going to and from these subtraits. +//! Since most data is passed around as `dyn PartialReflect` or `dyn Reflect` trait objects, +//! the `PartialReflect` trait has methods for going to and from these subtraits. //! -//! [`Reflect::reflect_kind`], [`Reflect::reflect_ref`], [`Reflect::reflect_mut`], and [`Reflect::reflect_owned`] all return +//! [`PartialReflect::reflect_kind`], [`PartialReflect::reflect_ref`], +//! [`PartialReflect::reflect_mut`], and [`PartialReflect::reflect_owned`] all return //! an enum that respectively contains zero-sized, immutable, mutable, and owned access to the type as a subtrait object. //! //! For example, we can get out a `dyn Tuple` from our reflected tuple type using one of these methods. //! //! ``` -//! # use bevy_reflect::{Reflect, ReflectRef}; -//! let my_tuple: Box = Box::new((1, 2, 3)); +//! # use bevy_reflect::{PartialReflect, ReflectRef}; +//! let my_tuple: Box = Box::new((1, 2, 3)); //! let ReflectRef::Tuple(my_tuple) = my_tuple.reflect_ref() else { unreachable!() }; //! assert_eq!(3, my_tuple.field_len()); //! ``` //! -//! And to go back to a general-purpose `dyn Reflect`, -//! we can just use the matching [`Reflect::as_reflect`], [`Reflect::as_reflect_mut`], -//! or [`Reflect::into_reflect`] methods. +//! And to go back to a general-purpose `dyn PartialReflect`, +//! we can just use the matching [`PartialReflect::as_partial_reflect`], [`PartialReflect::as_partial_reflect_mut`], +//! or [`PartialReflect::into_partial_reflect`] methods. //! //! ## Value Types //! @@ -120,7 +168,7 @@ //! such as for primitives (e.g. `bool`, `usize`, etc.) //! and simple types (e.g. `String`, `Duration`), //! are referred to as _value_ types -//! since methods like [`Reflect::reflect_ref`] return a [`ReflectRef::Value`] variant. +//! since methods like [`PartialReflect::reflect_ref`] return a [`ReflectRef::Value`] variant. //! While most other types contain their own `dyn Reflect` fields and data, //! these types generally cannot be broken down any further. //! @@ -143,18 +191,18 @@ //! # use bevy_reflect::{DynamicStruct, Struct}; //! let mut data = DynamicStruct::default(); //! data.insert("foo", 123_i32); -//! assert_eq!(Some(&123), data.field("foo").unwrap().downcast_ref::()) +//! assert_eq!(Some(&123), data.field("foo").unwrap().try_downcast_ref::()) //! ``` //! //! They are most commonly used as "proxies" for other types, //! where they contain the same data as— and therefore, represent— a concrete type. -//! The [`Reflect::clone_value`] method will return a dynamic type for all non-value types, +//! The [`PartialReflect::clone_value`] method will return a dynamic type for all non-value types, //! allowing all types to essentially be "cloned". -//! And since dynamic types themselves implement [`Reflect`], -//! we may pass them around just like any other reflected type. +//! And since dynamic types themselves implement [`PartialReflect`], +//! we may pass them around just like most other reflected types. //! //! ``` -//! # use bevy_reflect::{DynamicStruct, Reflect}; +//! # use bevy_reflect::{DynamicStruct, PartialReflect, Reflect}; //! # #[derive(Reflect)] //! # struct MyStruct { //! # foo: i32 @@ -164,18 +212,17 @@ //! }); //! //! // `cloned` will be a `DynamicStruct` representing a `MyStruct` -//! let cloned: Box = original.clone_value(); +//! let cloned: Box = original.clone_value(); //! assert!(cloned.represents::()); -//! assert!(cloned.is::()); //! ``` //! //! ## Patching //! //! These dynamic types come in handy when needing to apply multiple changes to another type. -//! This is known as "patching" and is done using the [`Reflect::apply`] and [`Reflect::try_apply`] methods. +//! This is known as "patching" and is done using the [`PartialReflect::apply`] and [`PartialReflect::try_apply`] methods. //! //! ``` -//! # use bevy_reflect::{DynamicEnum, Reflect}; +//! # use bevy_reflect::{DynamicEnum, PartialReflect}; //! let mut value = Some(123_i32); //! let patch = DynamicEnum::new("None", ()); //! value.apply(&patch); @@ -189,7 +236,7 @@ //! or when trying to make use of a reflected trait which expects the actual type. //! //! ```should_panic -//! # use bevy_reflect::{DynamicStruct, Reflect}; +//! # use bevy_reflect::{DynamicStruct, PartialReflect, Reflect}; //! # #[derive(Reflect)] //! # struct MyStruct { //! # foo: i32 @@ -198,8 +245,8 @@ //! foo: 123 //! }); //! -//! let cloned: Box = original.clone_value(); -//! let value = cloned.take::().unwrap(); // PANIC! +//! let cloned: Box = original.clone_value(); +//! let value = cloned.try_take::().unwrap(); // PANIC! //! ``` //! //! To resolve this issue, we'll need to convert the dynamic type to the concrete one. @@ -214,7 +261,7 @@ //! using `#[reflect(from_reflect = false)]` on the item. //! //! ``` -//! # use bevy_reflect::{Reflect, FromReflect}; +//! # use bevy_reflect::{FromReflect, PartialReflect, Reflect}; //! #[derive(Reflect)] //! struct MyStruct { //! foo: i32 @@ -223,7 +270,7 @@ //! foo: 123 //! }); //! -//! let cloned: Box = original.clone_value(); +//! let cloned: Box = original.clone_value(); //! let value = ::from_reflect(&*cloned).unwrap(); // OK! //! ``` //! @@ -239,7 +286,7 @@ //! //! # Path navigation //! -//! The [`GetPath`] trait allows accessing arbitrary nested fields of a [`Reflect`] type. +//! The [`GetPath`] trait allows accessing arbitrary nested fields of an [`PartialReflect`] type. //! //! Using `GetPath`, it is possible to use a path string to access a specific field //! of a reflected type. @@ -306,7 +353,8 @@ //! ## Reflecting Traits //! //! Type data doesn't have to be tied to a trait, but it's often extremely useful to create trait type data. -//! These allow traits to be used directly on a `dyn Reflect` while utilizing the underlying type's implementation. +//! These allow traits to be used directly on a `dyn Reflect` (and not a `dyn PartialReflect`) +//! while utilizing the underlying type's implementation. //! //! For any [object-safe] trait, we can easily generate a corresponding `ReflectTrait` type for our trait //! using the [`#[reflect_trait]`](reflect_trait) macro. @@ -358,7 +406,7 @@ //! # use serde::de::DeserializeSeed; //! # use bevy_reflect::{ //! # serde::{ReflectSerializer, ReflectDeserializer}, -//! # Reflect, FromReflect, TypeRegistry +//! # Reflect, PartialReflect, FromReflect, TypeRegistry //! # }; //! #[derive(Reflect, PartialEq, Debug)] //! struct MyStruct { @@ -374,12 +422,12 @@ //! registry.register::(); //! //! // Serialize -//! let reflect_serializer = ReflectSerializer::new(&original_value, ®istry); +//! let reflect_serializer = ReflectSerializer::new(original_value.as_partial_reflect(), ®istry); //! let serialized_value: String = ron::to_string(&reflect_serializer).unwrap(); //! //! // Deserialize //! let reflect_deserializer = ReflectDeserializer::new(®istry); -//! let deserialized_value: Box = reflect_deserializer.deserialize( +//! let deserialized_value: Box = reflect_deserializer.deserialize( //! &mut ron::Deserializer::from_str(&serialized_value).unwrap() //! ).unwrap(); //! @@ -449,7 +497,11 @@ //! [Bevy]: https://bevyengine.org/ //! [limitations]: #limitations //! [`bevy_reflect`]: crate +//! [introspection]: https://en.wikipedia.org/wiki/Type_introspection +//! [subtraits]: #the-reflection-subtraits +//! [the type registry]: #type-registration //! [runtime cost]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch +//! [the language feature for dyn upcasting coercion]: https://github.com/rust-lang/rust/issues/65991 //! [derive macro]: derive@crate::Reflect //! [`'static` lifetime]: https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html#trait-bound //! [derive macro documentation]: derive@crate::Reflect @@ -476,11 +528,14 @@ mod array; mod fields; mod from_reflect; +#[cfg(feature = "functions")] pub mod func; mod list; mod map; mod path; mod reflect; +mod remote; +mod set; mod struct_trait; mod tuple; mod tuple_struct; @@ -513,10 +568,13 @@ pub mod prelude { pub use crate::std_traits::*; #[doc(hidden)] pub use crate::{ - reflect_trait, FromReflect, GetField, GetPath, GetTupleStructField, Reflect, - ReflectDeserialize, ReflectFromReflect, ReflectPath, ReflectSerialize, Struct, TupleStruct, - TypePath, + reflect_trait, FromReflect, GetField, GetPath, GetTupleStructField, PartialReflect, + Reflect, ReflectDeserialize, ReflectFromReflect, ReflectPath, ReflectSerialize, Struct, + TupleStruct, TypePath, }; + + #[cfg(feature = "functions")] + pub use crate::func::{IntoFunction, IntoFunctionMut}; } pub use array::*; @@ -527,6 +585,8 @@ pub use list::*; pub use map::*; pub use path::*; pub use reflect::*; +pub use remote::*; +pub use set::*; pub use struct_trait::*; pub use tuple::*; pub use tuple_struct::*; @@ -726,10 +786,19 @@ mod tests { let mut map = DynamicMap::default(); map.insert(key_a, 10u32); - assert_eq!(10, *map.get(&key_b).unwrap().downcast_ref::().unwrap()); + assert_eq!( + 10, + *map.get(&key_b).unwrap().try_downcast_ref::().unwrap() + ); assert!(map.get(&key_c).is_none()); - *map.get_mut(&key_b).unwrap().downcast_mut::().unwrap() = 20; - assert_eq!(20, *map.get(&key_b).unwrap().downcast_ref::().unwrap()); + *map.get_mut(&key_b) + .unwrap() + .try_downcast_mut::() + .unwrap() = 20; + assert_eq!( + 20, + *map.get(&key_b).unwrap().try_downcast_ref::().unwrap() + ); } #[test] @@ -745,16 +814,22 @@ mod tests { let mut patch = DynamicTupleStruct::default(); patch.insert(3u32); patch.insert(4u64); - assert_eq!(3, *patch.field(0).unwrap().downcast_ref::().unwrap()); - assert_eq!(4, *patch.field(1).unwrap().downcast_ref::().unwrap()); + assert_eq!( + 3, + *patch.field(0).unwrap().try_downcast_ref::().unwrap() + ); + assert_eq!( + 4, + *patch.field(1).unwrap().try_downcast_ref::().unwrap() + ); foo.apply(&patch); assert_eq!(3, foo.0); assert_eq!(4, foo.1); let mut iter = patch.iter_fields(); - assert_eq!(3, *iter.next().unwrap().downcast_ref::().unwrap()); - assert_eq!(4, *iter.next().unwrap().downcast_ref::().unwrap()); + assert_eq!(3, *iter.next().unwrap().try_downcast_ref::().unwrap()); + assert_eq!(4, *iter.next().unwrap().try_downcast_ref::().unwrap()); } #[test] @@ -825,7 +900,7 @@ mod tests { let values: Vec = foo .iter_fields() - .map(|value| *value.downcast_ref::().unwrap()) + .map(|value| *value.try_downcast_ref::().unwrap()) .collect(); assert_eq!(values, vec![1]); } @@ -857,11 +932,11 @@ mod tests { // Assert let expected = MyStruct { foo: 123 }; assert!(expected - .reflect_partial_eq(reflected.as_ref()) + .reflect_partial_eq(reflected.as_partial_reflect()) .unwrap_or_default()); let not_expected = MyStruct { foo: 321 }; assert!(!not_expected - .reflect_partial_eq(reflected.as_ref()) + .reflect_partial_eq(reflected.as_partial_reflect()) .unwrap_or_default()); } @@ -1088,7 +1163,7 @@ mod tests { }); foo_patch.insert("g", composite); - let array = DynamicArray::from_vec(vec![2u32, 2u32]); + let array = DynamicArray::from_iter([2u32, 2u32]); foo_patch.insert("h", array); foo.apply(&foo_patch); @@ -1308,9 +1383,9 @@ mod tests { let mut deserializer = Deserializer::from_str(&serialized).unwrap(); let reflect_deserializer = ReflectDeserializer::new(®istry); let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); - let dynamic_struct = value.take::().unwrap(); + let roundtrip_foo = Foo::from_reflect(value.as_partial_reflect()).unwrap(); - assert!(foo.reflect_partial_eq(&dynamic_struct).unwrap()); + assert!(foo.reflect_partial_eq(&roundtrip_foo).unwrap()); } #[test] @@ -1546,18 +1621,15 @@ mod tests { bar: usize, } - let info = MyStruct::type_info(); - if let TypeInfo::Struct(info) = info { - assert!(info.is::()); - assert_eq!(MyStruct::type_path(), info.type_path()); - assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path()); - assert_eq!(TypeId::of::(), info.field("foo").unwrap().type_id()); - assert!(info.field("foo").unwrap().is::()); - assert_eq!("foo", info.field("foo").unwrap().name()); - assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path()); - } else { - panic!("Expected `TypeInfo::Struct`"); - } + let info = MyStruct::type_info().as_struct().unwrap(); + assert!(info.is::()); + assert_eq!(MyStruct::type_path(), info.type_path()); + assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path()); + assert_eq!(TypeId::of::(), info.field("foo").unwrap().type_id()); + assert!(info.field("foo").unwrap().type_info().unwrap().is::()); + assert!(info.field("foo").unwrap().is::()); + assert_eq!("foo", info.field("foo").unwrap().name()); + assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path()); let value: &dyn Reflect = &MyStruct { foo: 123, bar: 321 }; let info = value.get_represented_type_info().unwrap(); @@ -1570,49 +1642,72 @@ mod tests { bar: usize, } - let info = >::type_info(); + let info = >::type_info().as_struct().unwrap(); + assert!(info.is::>()); + assert_eq!(MyGenericStruct::::type_path(), info.type_path()); + assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path()); + assert_eq!("foo", info.field("foo").unwrap().name()); + assert!(info.field("foo").unwrap().type_info().unwrap().is::()); + assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path()); + + let value: &dyn Reflect = &MyGenericStruct { + foo: String::from("Hello!"), + bar: 321, + }; + let info = value.get_represented_type_info().unwrap(); + assert!(info.is::>()); + + // Struct (dynamic field) + #[derive(Reflect)] + #[reflect(from_reflect = false)] + struct MyDynamicStruct { + foo: DynamicStruct, + bar: usize, + } + + let info = MyDynamicStruct::type_info(); if let TypeInfo::Struct(info) = info { - assert!(info.is::>()); - assert_eq!(MyGenericStruct::::type_path(), info.type_path()); - assert_eq!(i32::type_path(), info.field("foo").unwrap().type_path()); + assert!(info.is::()); + assert_eq!(MyDynamicStruct::type_path(), info.type_path()); + assert_eq!( + DynamicStruct::type_path(), + info.field("foo").unwrap().type_path() + ); assert_eq!("foo", info.field("foo").unwrap().name()); + assert!(info.field("foo").unwrap().type_info().is_none()); assert_eq!(usize::type_path(), info.field_at(1).unwrap().type_path()); } else { panic!("Expected `TypeInfo::Struct`"); } - let value: &dyn Reflect = &MyGenericStruct { - foo: String::from("Hello!"), + let value: &dyn Reflect = &MyDynamicStruct { + foo: DynamicStruct::default(), bar: 321, }; let info = value.get_represented_type_info().unwrap(); - assert!(info.is::>()); + assert!(info.is::()); // Tuple Struct #[derive(Reflect)] struct MyTupleStruct(usize, i32, MyStruct); - let info = MyTupleStruct::type_info(); - if let TypeInfo::TupleStruct(info) = info { - assert!(info.is::()); - assert_eq!(MyTupleStruct::type_path(), info.type_path()); - assert_eq!(i32::type_path(), info.field_at(1).unwrap().type_path()); - assert!(info.field_at(1).unwrap().is::()); - } else { - panic!("Expected `TypeInfo::TupleStruct`"); - } + let info = MyTupleStruct::type_info().as_tuple_struct().unwrap(); + + assert!(info.is::()); + assert_eq!(MyTupleStruct::type_path(), info.type_path()); + assert_eq!(i32::type_path(), info.field_at(1).unwrap().type_path()); + assert!(info.field_at(1).unwrap().type_info().unwrap().is::()); + assert!(info.field_at(1).unwrap().is::()); // Tuple type MyTuple = (u32, f32, String); - let info = MyTuple::type_info(); - if let TypeInfo::Tuple(info) = info { - assert!(info.is::()); - assert_eq!(MyTuple::type_path(), info.type_path()); - assert_eq!(f32::type_path(), info.field_at(1).unwrap().type_path()); - } else { - panic!("Expected `TypeInfo::Tuple`"); - } + let info = MyTuple::type_info().as_tuple().unwrap(); + + assert!(info.is::()); + assert_eq!(MyTuple::type_path(), info.type_path()); + assert_eq!(f32::type_path(), info.field_at(1).unwrap().type_path()); + assert!(info.field_at(1).unwrap().type_info().unwrap().is::()); let value: &dyn Reflect = &(123_u32, 1.23_f32, String::from("Hello!")); let info = value.get_represented_type_info().unwrap(); @@ -1621,15 +1716,13 @@ mod tests { // List type MyList = Vec; - let info = MyList::type_info(); - if let TypeInfo::List(info) = info { - assert!(info.is::()); - assert!(info.item_is::()); - assert_eq!(MyList::type_path(), info.type_path()); - assert_eq!(usize::type_path(), info.item_type_path_table().path()); - } else { - panic!("Expected `TypeInfo::List`"); - } + let info = MyList::type_info().as_list().unwrap(); + + assert!(info.is::()); + assert!(info.item_is::()); + assert!(info.item_info().unwrap().is::()); + assert_eq!(MyList::type_path(), info.type_path()); + assert_eq!(usize::type_path(), info.item_type_path_table().path()); let value: &dyn Reflect = &vec![123_usize]; let info = value.get_represented_type_info().unwrap(); @@ -1640,15 +1733,12 @@ mod tests { { type MySmallVec = smallvec::SmallVec<[String; 2]>; - let info = MySmallVec::type_info(); - if let TypeInfo::List(info) = info { - assert!(info.is::()); - assert!(info.item_is::()); - assert_eq!(MySmallVec::type_path(), info.type_path()); - assert_eq!(String::type_path(), info.item_type_path_table().path()); - } else { - panic!("Expected `TypeInfo::List`"); - } + let info = MySmallVec::type_info().as_list().unwrap(); + assert!(info.is::()); + assert!(info.item_is::()); + assert!(info.item_info().unwrap().is::()); + assert_eq!(MySmallVec::type_path(), info.type_path()); + assert_eq!(String::type_path(), info.item_type_path_table().path()); let value: MySmallVec = smallvec::smallvec![String::default(); 2]; let value: &dyn Reflect = &value; @@ -1659,16 +1749,13 @@ mod tests { // Array type MyArray = [usize; 3]; - let info = MyArray::type_info(); - if let TypeInfo::Array(info) = info { - assert!(info.is::()); - assert!(info.item_is::()); - assert_eq!(MyArray::type_path(), info.type_path()); - assert_eq!(usize::type_path(), info.item_type_path_table().path()); - assert_eq!(3, info.capacity()); - } else { - panic!("Expected `TypeInfo::Array`"); - } + let info = MyArray::type_info().as_array().unwrap(); + assert!(info.is::()); + assert!(info.item_is::()); + assert!(info.item_info().unwrap().is::()); + assert_eq!(MyArray::type_path(), info.type_path()); + assert_eq!(usize::type_path(), info.item_type_path_table().path()); + assert_eq!(3, info.capacity()); let value: &dyn Reflect = &[1usize, 2usize, 3usize]; let info = value.get_represented_type_info().unwrap(); @@ -1677,13 +1764,10 @@ mod tests { // Cow<'static, str> type MyCowStr = Cow<'static, str>; - let info = MyCowStr::type_info(); - if let TypeInfo::Value(info) = info { - assert!(info.is::()); - assert_eq!(std::any::type_name::(), info.type_path()); - } else { - panic!("Expected `TypeInfo::Value`"); - } + let info = MyCowStr::type_info().as_value().unwrap(); + + assert!(info.is::()); + assert_eq!(std::any::type_name::(), info.type_path()); let value: &dyn Reflect = &Cow::<'static, str>::Owned("Hello!".to_string()); let info = value.get_represented_type_info().unwrap(); @@ -1692,18 +1776,16 @@ mod tests { // Cow<'static, [u8]> type MyCowSlice = Cow<'static, [u8]>; - let info = MyCowSlice::type_info(); - if let TypeInfo::List(info) = info { - assert!(info.is::()); - assert!(info.item_is::()); - assert_eq!(std::any::type_name::(), info.type_path()); - assert_eq!( - std::any::type_name::(), - info.item_type_path_table().path() - ); - } else { - panic!("Expected `TypeInfo::List`"); - } + let info = MyCowSlice::type_info().as_list().unwrap(); + + assert!(info.is::()); + assert!(info.item_is::()); + assert!(info.item_info().unwrap().is::()); + assert_eq!(std::any::type_name::(), info.type_path()); + assert_eq!( + std::any::type_name::(), + info.item_type_path_table().path() + ); let value: &dyn Reflect = &Cow::<'static, [u8]>::Owned(vec![0, 1, 2, 3]); let info = value.get_represented_type_info().unwrap(); @@ -1712,17 +1794,16 @@ mod tests { // Map type MyMap = HashMap; - let info = MyMap::type_info(); - if let TypeInfo::Map(info) = info { - assert!(info.is::()); - assert!(info.key_is::()); - assert!(info.value_is::()); - assert_eq!(MyMap::type_path(), info.type_path()); - assert_eq!(usize::type_path(), info.key_type_path_table().path()); - assert_eq!(f32::type_path(), info.value_type_path_table().path()); - } else { - panic!("Expected `TypeInfo::Map`"); - } + let info = MyMap::type_info().as_map().unwrap(); + + assert!(info.is::()); + assert!(info.key_is::()); + assert!(info.value_is::()); + assert!(info.key_info().unwrap().is::()); + assert!(info.value_info().unwrap().is::()); + assert_eq!(MyMap::type_path(), info.type_path()); + assert_eq!(usize::type_path(), info.key_type_path_table().path()); + assert_eq!(f32::type_path(), info.value_type_path_table().path()); let value: &dyn Reflect = &MyMap::new(); let info = value.get_represented_type_info().unwrap(); @@ -1731,13 +1812,10 @@ mod tests { // Value type MyValue = String; - let info = MyValue::type_info(); - if let TypeInfo::Value(info) = info { - assert!(info.is::()); - assert_eq!(MyValue::type_path(), info.type_path()); - } else { - panic!("Expected `TypeInfo::Value`"); - } + let info = MyValue::type_info().as_value().unwrap(); + + assert!(info.is::()); + assert_eq!(MyValue::type_path(), info.type_path()); let value: &dyn Reflect = &String::from("Hello!"); let info = value.get_represented_type_info().unwrap(); @@ -1877,15 +1955,12 @@ mod tests { data: Vec, } - let info = ::type_info(); - if let TypeInfo::Struct(info) = info { - let mut fields = info.iter(); - assert_eq!(Some(" The name"), fields.next().unwrap().docs()); - assert_eq!(Some(" The index"), fields.next().unwrap().docs()); - assert_eq!(None, fields.next().unwrap().docs()); - } else { - panic!("expected struct info"); - } + let info = ::type_info().as_struct().unwrap(); + + let mut fields = info.iter(); + assert_eq!(Some(" The name"), fields.next().unwrap().docs()); + assert_eq!(Some(" The index"), fields.next().unwrap().docs()); + assert_eq!(None, fields.next().unwrap().docs()); } #[test] @@ -1906,31 +1981,20 @@ mod tests { }, } - let info = ::type_info(); - if let TypeInfo::Enum(info) = info { - let mut variants = info.iter(); - assert_eq!(None, variants.next().unwrap().docs()); - - let variant = variants.next().unwrap(); - assert_eq!(Some(" Option A"), variant.docs()); - if let VariantInfo::Tuple(variant) = variant { - let field = variant.field_at(0).unwrap(); - assert_eq!(Some(" Index"), field.docs()); - } else { - panic!("expected tuple variant") - } + let info = ::type_info().as_enum().unwrap(); - let variant = variants.next().unwrap(); - assert_eq!(Some(" Option B"), variant.docs()); - if let VariantInfo::Struct(variant) = variant { - let field = variant.field_at(0).unwrap(); - assert_eq!(Some(" Name"), field.docs()); - } else { - panic!("expected struct variant") - } - } else { - panic!("expected enum info"); - } + let mut variants = info.iter(); + assert_eq!(None, variants.next().unwrap().docs()); + + let variant = variants.next().unwrap().as_tuple_variant().unwrap(); + assert_eq!(Some(" Option A"), variant.docs()); + let field = variant.field_at(0).unwrap(); + assert_eq!(Some(" Index"), field.docs()); + + let variant = variants.next().unwrap().as_struct_variant().unwrap(); + assert_eq!(Some(" Option B"), variant.docs()); + let field = variant.field_at(0).unwrap(); + assert_eq!(Some(" Name"), field.docs()); } } @@ -2076,7 +2140,7 @@ bevy_reflect::tests::Test { } let foo = Foo(123); - let foo: &dyn Reflect = &foo; + let foo: &dyn PartialReflect = &foo; assert!(foo.reflect_hash().is_some()); assert_eq!(Some(true), foo.reflect_partial_eq(foo)); @@ -2097,7 +2161,7 @@ bevy_reflect::tests::Test { } let foo = Foo(123); - let foo: &dyn Reflect = &foo; + let foo: &dyn PartialReflect = &foo; assert!(foo.reflect_hash().is_some()); assert_eq!(Some(true), foo.reflect_partial_eq(foo)); @@ -2346,7 +2410,7 @@ bevy_reflect::tests::Test { } let mut map = HashMap::new(); map.insert(9, 10); - let mut test_struct = TestStruct { + let mut test_struct: DynamicStruct = TestStruct { tuple: (0, 1), list: vec![2, 3, 4], array: [5, 6, 7], @@ -2355,8 +2419,7 @@ bevy_reflect::tests::Test { map, value: 12, } - .clone_value(); - let test_struct = test_struct.downcast_mut::().unwrap(); + .clone_dynamic(); // test unknown DynamicStruct let mut test_unknown_struct = DynamicStruct::default(); @@ -2418,6 +2481,447 @@ bevy_reflect::tests::Test { assert_impl_all!(Enum: Reflect); } + #[test] + fn should_reflect_remote_type() { + mod external_crate { + #[derive(Debug, Default)] + pub struct TheirType { + pub value: String, + } + } + + // === Remote Wrapper === // + #[reflect_remote(external_crate::TheirType)] + #[derive(Debug, Default)] + #[reflect(Debug, Default)] + struct MyType { + pub value: String, + } + + let mut patch = DynamicStruct::default(); + patch.set_represented_type(Some(MyType::type_info())); + patch.insert("value", "Goodbye".to_string()); + + let mut data = MyType(external_crate::TheirType { + value: "Hello".to_string(), + }); + + assert_eq!("Hello", data.0.value); + data.apply(&patch); + assert_eq!("Goodbye", data.0.value); + + // === Struct Container === // + #[derive(Reflect, Debug)] + #[reflect(from_reflect = false)] + struct ContainerStruct { + #[reflect(remote = MyType)] + their_type: external_crate::TheirType, + } + + let mut patch = DynamicStruct::default(); + patch.set_represented_type(Some(ContainerStruct::type_info())); + patch.insert( + "their_type", + MyType(external_crate::TheirType { + value: "Goodbye".to_string(), + }), + ); + + let mut data = ContainerStruct { + their_type: external_crate::TheirType { + value: "Hello".to_string(), + }, + }; + + assert_eq!("Hello", data.their_type.value); + data.apply(&patch); + assert_eq!("Goodbye", data.their_type.value); + + // === Tuple Struct Container === // + #[derive(Reflect, Debug)] + struct ContainerTupleStruct(#[reflect(remote = MyType)] external_crate::TheirType); + + let mut patch = DynamicTupleStruct::default(); + patch.set_represented_type(Some(ContainerTupleStruct::type_info())); + patch.insert(MyType(external_crate::TheirType { + value: "Goodbye".to_string(), + })); + + let mut data = ContainerTupleStruct(external_crate::TheirType { + value: "Hello".to_string(), + }); + + assert_eq!("Hello", data.0.value); + data.apply(&patch); + assert_eq!("Goodbye", data.0.value); + } + + #[test] + fn should_reflect_remote_value_type() { + mod external_crate { + #[derive(Clone, Debug, Default)] + pub struct TheirType { + pub value: String, + } + } + + // === Remote Wrapper === // + #[reflect_remote(external_crate::TheirType)] + #[derive(Clone, Debug, Default)] + #[reflect_value(Debug, Default)] + struct MyType { + pub value: String, + } + + let mut data = MyType(external_crate::TheirType { + value: "Hello".to_string(), + }); + + let patch = MyType(external_crate::TheirType { + value: "Goodbye".to_string(), + }); + + assert_eq!("Hello", data.0.value); + data.apply(&patch); + assert_eq!("Goodbye", data.0.value); + + // === Struct Container === // + #[derive(Reflect, Debug)] + #[reflect(from_reflect = false)] + struct ContainerStruct { + #[reflect(remote = MyType)] + their_type: external_crate::TheirType, + } + + let mut patch = DynamicStruct::default(); + patch.set_represented_type(Some(ContainerStruct::type_info())); + patch.insert( + "their_type", + MyType(external_crate::TheirType { + value: "Goodbye".to_string(), + }), + ); + + let mut data = ContainerStruct { + their_type: external_crate::TheirType { + value: "Hello".to_string(), + }, + }; + + assert_eq!("Hello", data.their_type.value); + data.apply(&patch); + assert_eq!("Goodbye", data.their_type.value); + + // === Tuple Struct Container === // + #[derive(Reflect, Debug)] + struct ContainerTupleStruct(#[reflect(remote = MyType)] external_crate::TheirType); + + let mut patch = DynamicTupleStruct::default(); + patch.set_represented_type(Some(ContainerTupleStruct::type_info())); + patch.insert(MyType(external_crate::TheirType { + value: "Goodbye".to_string(), + })); + + let mut data = ContainerTupleStruct(external_crate::TheirType { + value: "Hello".to_string(), + }); + + assert_eq!("Hello", data.0.value); + data.apply(&patch); + assert_eq!("Goodbye", data.0.value); + } + + #[test] + fn should_reflect_remote_type_from_module() { + mod wrapper { + use super::*; + + // We have to place this module internally to this one to get around the following error: + // ``` + // error[E0433]: failed to resolve: use of undeclared crate or module `external_crate` + // ``` + pub mod external_crate { + pub struct TheirType { + pub value: String, + } + } + + #[reflect_remote(external_crate::TheirType)] + pub struct MyType { + pub value: String, + } + } + + #[derive(Reflect)] + struct ContainerStruct { + #[reflect(remote = wrapper::MyType)] + their_type: wrapper::external_crate::TheirType, + } + } + + #[test] + fn should_reflect_remote_enum() { + mod external_crate { + #[derive(Debug, PartialEq, Eq)] + pub enum TheirType { + Unit, + Tuple(usize), + Struct { value: String }, + } + } + + // === Remote Wrapper === // + #[reflect_remote(external_crate::TheirType)] + #[derive(Debug)] + #[reflect(Debug)] + enum MyType { + Unit, + Tuple(usize), + Struct { value: String }, + } + + let mut patch = DynamicEnum::from(MyType(external_crate::TheirType::Tuple(123))); + + let mut data = MyType(external_crate::TheirType::Unit); + + assert_eq!(external_crate::TheirType::Unit, data.0); + data.apply(&patch); + assert_eq!(external_crate::TheirType::Tuple(123), data.0); + + patch = DynamicEnum::from(MyType(external_crate::TheirType::Struct { + value: "Hello world!".to_string(), + })); + + data.apply(&patch); + assert_eq!( + external_crate::TheirType::Struct { + value: "Hello world!".to_string() + }, + data.0 + ); + + // === Enum Container === // + #[derive(Reflect, Debug, PartialEq)] + enum ContainerEnum { + Foo, + Bar { + #[reflect(remote = MyType)] + their_type: external_crate::TheirType, + }, + } + + let patch = DynamicEnum::from(ContainerEnum::Bar { + their_type: external_crate::TheirType::Tuple(123), + }); + + let mut data = ContainerEnum::Foo; + + assert_eq!(ContainerEnum::Foo, data); + data.apply(&patch); + assert_eq!( + ContainerEnum::Bar { + their_type: external_crate::TheirType::Tuple(123) + }, + data + ); + } + + #[test] + fn should_reflect_nested_remote_type() { + mod external_crate { + pub struct TheirOuter { + pub a: TheirInner, + pub b: TheirInner, + } + + pub struct TheirInner(pub T); + } + + #[reflect_remote(external_crate::TheirOuter)] + struct MyOuter { + #[reflect(remote = MyInner)] + pub a: external_crate::TheirInner, + #[reflect(remote = MyInner)] + pub b: external_crate::TheirInner, + } + + #[reflect_remote(external_crate::TheirInner)] + struct MyInner(pub T); + + let mut patch = DynamicStruct::default(); + patch.set_represented_type(Some(MyOuter::::type_info())); + patch.insert("a", MyInner(external_crate::TheirInner(321_i32))); + patch.insert("b", MyInner(external_crate::TheirInner(true))); + + let mut data = MyOuter(external_crate::TheirOuter { + a: external_crate::TheirInner(123_i32), + b: external_crate::TheirInner(false), + }); + + assert_eq!(123, data.0.a.0); + assert!(!data.0.b.0); + data.apply(&patch); + assert_eq!(321, data.0.a.0); + assert!(data.0.b.0); + } + + #[test] + fn should_reflect_nested_remote_enum() { + mod external_crate { + use std::fmt::Debug; + + #[derive(Debug)] + pub enum TheirOuter { + Unit, + Tuple(TheirInner), + Struct { value: TheirInner }, + } + #[derive(Debug)] + pub enum TheirInner { + Unit, + Tuple(T), + Struct { value: T }, + } + } + + #[reflect_remote(external_crate::TheirOuter)] + #[derive(Debug)] + enum MyOuter { + Unit, + Tuple(#[reflect(remote = MyInner)] external_crate::TheirInner), + Struct { + #[reflect(remote = MyInner)] + value: external_crate::TheirInner, + }, + } + + #[reflect_remote(external_crate::TheirInner)] + #[derive(Debug)] + enum MyInner { + Unit, + Tuple(T), + Struct { value: T }, + } + + let mut patch = DynamicEnum::default(); + let mut value = DynamicStruct::default(); + value.insert("value", MyInner(external_crate::TheirInner::Tuple(123))); + patch.set_variant("Struct", value); + + let mut data = MyOuter(external_crate::TheirOuter::::Unit); + + assert!(matches!( + data, + MyOuter(external_crate::TheirOuter::::Unit) + )); + data.apply(&patch); + assert!(matches!( + data, + MyOuter(external_crate::TheirOuter::Struct { + value: external_crate::TheirInner::Tuple(123) + }) + )); + } + + #[test] + fn should_take_remote_type() { + mod external_crate { + #[derive(Debug, Default, PartialEq, Eq)] + pub struct TheirType { + pub value: String, + } + } + + // === Remote Wrapper === // + #[reflect_remote(external_crate::TheirType)] + #[derive(Debug, Default)] + #[reflect(Debug, Default)] + struct MyType { + pub value: String, + } + + let input: Box = Box::new(MyType(external_crate::TheirType { + value: "Hello".to_string(), + })); + + let output: external_crate::TheirType = input + .take() + .expect("should downcast to `external_crate::TheirType`"); + assert_eq!( + external_crate::TheirType { + value: "Hello".to_string(), + }, + output + ); + } + + #[test] + fn should_try_take_remote_type() { + mod external_crate { + #[derive(Debug, Default, PartialEq, Eq)] + pub struct TheirType { + pub value: String, + } + } + + // === Remote Wrapper === // + #[reflect_remote(external_crate::TheirType)] + #[derive(Debug, Default)] + #[reflect(Debug, Default)] + struct MyType { + pub value: String, + } + + let input: Box = Box::new(MyType(external_crate::TheirType { + value: "Hello".to_string(), + })); + + let output: external_crate::TheirType = input + .try_take() + .expect("should downcast to `external_crate::TheirType`"); + assert_eq!( + external_crate::TheirType { + value: "Hello".to_string(), + }, + output, + ); + } + + #[test] + fn should_take_nested_remote_type() { + mod external_crate { + #[derive(PartialEq, Eq, Debug)] + pub struct TheirOuter { + pub inner: TheirInner, + } + #[derive(PartialEq, Eq, Debug)] + pub struct TheirInner(pub T); + } + + #[reflect_remote(external_crate::TheirOuter)] + struct MyOuter { + #[reflect(remote = MyInner)] + pub inner: external_crate::TheirInner, + } + + #[reflect_remote(external_crate::TheirInner)] + struct MyInner(pub T); + + let input: Box = Box::new(MyOuter(external_crate::TheirOuter { + inner: external_crate::TheirInner(123), + })); + + let output: external_crate::TheirOuter = input + .take() + .expect("should downcast to `external_crate::TheirOuter`"); + assert_eq!( + external_crate::TheirOuter { + inner: external_crate::TheirInner(123), + }, + output + ); + } + #[cfg(feature = "glam")] mod glam { use super::*; @@ -2477,7 +2981,7 @@ bevy_reflect::tests::Test { let mut result = Quat::default(); - result.apply(&*dynamic_struct); + result.apply(dynamic_struct.as_partial_reflect()); assert_eq!(result, quat(1.0, 2.0, 3.0, 4.0)); } @@ -2534,7 +3038,7 @@ bevy_reflect::tests::Test { let mut result = Vec3::default(); - result.apply(&*dynamic_struct); + result.apply(dynamic_struct.as_partial_reflect()); assert_eq!(result, vec3(12.0, 3.0, -6.9)); } @@ -2555,13 +3059,16 @@ bevy_reflect::tests::Test { let mut v = vec3(1.0, 2.0, 3.0); assert_eq!( - *v.reflect_path("x").unwrap().downcast_ref::().unwrap(), + *v.reflect_path("x") + .unwrap() + .try_downcast_ref::() + .unwrap(), 1.0 ); *v.reflect_path_mut("y") .unwrap() - .downcast_mut::() + .try_downcast_mut::() .unwrap() = 6.0; assert_eq!(v.y, 6.0); diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index c73baab3299b9d..3a29c5733e6591 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -4,11 +4,10 @@ use std::hash::{Hash, Hasher}; use bevy_reflect_derive::impl_type_path; -use crate::func::macros::impl_function_traits; use crate::utility::reflect_hasher; use crate::{ - self as bevy_reflect, ApplyError, FromReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, - ReflectRef, TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, FromReflect, MaybeTyped, PartialReflect, Reflect, + ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, }; /// A trait used to power [list-like] operations via [reflection]. @@ -36,46 +35,46 @@ use crate::{ /// # Example /// /// ``` -/// use bevy_reflect::{Reflect, List}; +/// use bevy_reflect::{PartialReflect, Reflect, List}; /// /// let foo: &mut dyn List = &mut vec![123_u32, 456_u32, 789_u32]; /// assert_eq!(foo.len(), 3); /// -/// let last_field: Box = foo.pop().unwrap(); -/// assert_eq!(last_field.downcast_ref::(), Some(&789)); +/// let last_field: Box = foo.pop().unwrap(); +/// assert_eq!(last_field.try_downcast_ref::(), Some(&789)); /// ``` /// /// [list-like]: https://doc.rust-lang.org/book/ch08-01-vectors.html /// [reflection]: crate /// [type-erasing]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html -pub trait List: Reflect { +pub trait List: PartialReflect { /// Returns a reference to the element at `index`, or `None` if out of bounds. - fn get(&self, index: usize) -> Option<&dyn Reflect>; + fn get(&self, index: usize) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the element at `index`, or `None` if out of bounds. - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect>; + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect>; /// Inserts an element at position `index` within the list, /// shifting all elements after it towards the back of the list. /// /// # Panics /// Panics if `index > len`. - fn insert(&mut self, index: usize, element: Box); + fn insert(&mut self, index: usize, element: Box); /// Removes and returns the element at position `index` within the list, /// shifting all elements before it towards the front of the list. /// /// # Panics /// Panics if `index` is out of bounds. - fn remove(&mut self, index: usize) -> Box; + fn remove(&mut self, index: usize) -> Box; /// Appends an element to the _back_ of the list. - fn push(&mut self, value: Box) { + fn push(&mut self, value: Box) { self.insert(self.len(), value); } /// Removes the _back_ element from the list and returns it, or [`None`] if it is empty. - fn pop(&mut self) -> Option> { + fn pop(&mut self) -> Option> { if self.is_empty() { None } else { @@ -95,13 +94,13 @@ pub trait List: Reflect { fn iter(&self) -> ListIter; /// Drain the elements of this list to get a vector of owned values. - fn drain(self: Box) -> Vec>; + fn drain(self: Box) -> Vec>; /// Clones the list, producing a [`DynamicList`]. fn clone_dynamic(&self) -> DynamicList { DynamicList { represented_type: self.get_represented_type_info(), - values: self.iter().map(Reflect::clone_value).collect(), + values: self.iter().map(PartialReflect::clone_value).collect(), } } } @@ -111,6 +110,7 @@ pub trait List: Reflect { pub struct ListInfo { type_path: TypePathTable, type_id: TypeId, + item_info: fn() -> Option<&'static TypeInfo>, item_type_path: TypePathTable, item_type_id: TypeId, #[cfg(feature = "documentation")] @@ -119,10 +119,11 @@ pub struct ListInfo { impl ListInfo { /// Create a new [`ListInfo`]. - pub fn new() -> Self { + pub fn new() -> Self { Self { type_path: TypePathTable::of::(), type_id: TypeId::of::(), + item_info: TItem::maybe_type_info, item_type_path: TypePathTable::of::(), item_type_id: TypeId::of::(), #[cfg(feature = "documentation")] @@ -163,6 +164,14 @@ impl ListInfo { TypeId::of::() == self.type_id } + /// The [`TypeInfo`] of the list item. + /// + /// Returns `None` if the list item does not contain static type information, + /// such as for dynamic types. + pub fn item_info(&self) -> Option<&'static TypeInfo> { + (self.item_info)() + } + /// A representation of the type path of the list item. /// /// Provides dynamic access to all methods on [`TypePath`]. @@ -191,7 +200,7 @@ impl ListInfo { #[derive(Default)] pub struct DynamicList { represented_type: Option<&'static TypeInfo>, - values: Vec>, + values: Vec>, } impl DynamicList { @@ -214,38 +223,38 @@ impl DynamicList { } /// Appends a typed value to the list. - pub fn push(&mut self, value: T) { + pub fn push(&mut self, value: T) { self.values.push(Box::new(value)); } /// Appends a [`Reflect`] trait object to the list. - pub fn push_box(&mut self, value: Box) { + pub fn push_box(&mut self, value: Box) { self.values.push(value); } } impl List for DynamicList { - fn get(&self, index: usize) -> Option<&dyn Reflect> { + fn get(&self, index: usize) -> Option<&dyn PartialReflect> { self.values.get(index).map(|value| &**value) } - fn get_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn get_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { self.values.get_mut(index).map(|value| &mut **value) } - fn insert(&mut self, index: usize, element: Box) { + fn insert(&mut self, index: usize, element: Box) { self.values.insert(index, element); } - fn remove(&mut self, index: usize) -> Box { + fn remove(&mut self, index: usize) -> Box { self.values.remove(index) } - fn push(&mut self, value: Box) { + fn push(&mut self, value: Box) { DynamicList::push_box(self, value); } - fn pop(&mut self) -> Option> { + fn pop(&mut self) -> Option> { self.values.pop() } @@ -257,7 +266,7 @@ impl List for DynamicList { ListIter::new(self) } - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { self.values } @@ -273,56 +282,47 @@ impl List for DynamicList { } } -impl Reflect for DynamicList { +impl PartialReflect for DynamicList { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } - fn apply(&mut self, value: &dyn Reflect) { + fn apply(&mut self, value: &dyn PartialReflect) { list_apply(self, value); } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { list_try_apply(self, value) } - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - #[inline] fn reflect_kind(&self) -> ReflectKind { ReflectKind::List @@ -344,7 +344,7 @@ impl Reflect for DynamicList { } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } @@ -353,7 +353,7 @@ impl Reflect for DynamicList { list_hash(self) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { list_partial_eq(self, value) } @@ -370,7 +370,6 @@ impl Reflect for DynamicList { } impl_type_path!((in bevy_reflect) DynamicList); -impl_function_traits!(DynamicList); impl Debug for DynamicList { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -378,8 +377,26 @@ impl Debug for DynamicList { } } +impl FromIterator> for DynamicList { + fn from_iter>>(values: I) -> Self { + Self { + represented_type: None, + values: values.into_iter().collect(), + } + } +} + +impl FromIterator for DynamicList { + fn from_iter>(values: I) -> Self { + values + .into_iter() + .map(|field| Box::new(field).into_partial_reflect()) + .collect() + } +} + impl IntoIterator for DynamicList { - type Item = Box; + type Item = Box; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { @@ -387,6 +404,15 @@ impl IntoIterator for DynamicList { } } +impl<'a> IntoIterator for &'a DynamicList { + type Item = &'a dyn PartialReflect; + type IntoIter = ListIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + /// An iterator over an [`List`]. pub struct ListIter<'a> { list: &'a dyn List, @@ -402,7 +428,7 @@ impl<'a> ListIter<'a> { } impl<'a> Iterator for ListIter<'a> { - type Item = &'a dyn Reflect; + type Item = &'a dyn PartialReflect; #[inline] fn next(&mut self) -> Option { @@ -441,7 +467,7 @@ pub fn list_hash(list: &L) -> Option { /// /// This function panics if `b` is not a list. #[inline] -pub fn list_apply(a: &mut L, b: &dyn Reflect) { +pub fn list_apply(a: &mut L, b: &dyn PartialReflect) { if let Err(err) = list_try_apply(a, b) { panic!("{err}"); } @@ -458,7 +484,7 @@ pub fn list_apply(a: &mut L, b: &dyn Reflect) { /// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a list or if /// applying elements to each other fails. #[inline] -pub fn list_try_apply(a: &mut L, b: &dyn Reflect) -> Result<(), ApplyError> { +pub fn list_try_apply(a: &mut L, b: &dyn PartialReflect) -> Result<(), ApplyError> { if let ReflectRef::List(list_value) = b.reflect_ref() { for (i, value) in list_value.iter().enumerate() { if i < a.len() { @@ -483,11 +509,11 @@ pub fn list_try_apply(a: &mut L, b: &dyn Reflect) -> Result<(), ApplyEr /// Returns true if and only if all of the following are true: /// - `b` is a list; /// - `b` is the same length as `a`; -/// - [`Reflect::reflect_partial_eq`] returns `Some(true)` for pairwise elements of `a` and `b`. +/// - [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for pairwise elements of `a` and `b`. /// /// Returns [`None`] if the comparison couldn't even be performed. #[inline] -pub fn list_partial_eq(a: &L, b: &dyn Reflect) -> Option { +pub fn list_partial_eq(a: &L, b: &dyn PartialReflect) -> Option { let ReflectRef::List(list) = b.reflect_ref() else { return Some(false); }; @@ -546,7 +572,9 @@ mod tests { list.push(2usize); let items = list.into_iter(); for (index, item) in items.into_iter().enumerate() { - let value = item.take::().expect("couldn't downcast to usize"); + let value = item + .try_take::() + .expect("couldn't downcast to usize"); assert_eq!(index, value); } } diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 844be9a2476be7..b619a091eff2c9 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -4,10 +4,9 @@ use std::fmt::{Debug, Formatter}; use bevy_reflect_derive::impl_type_path; use bevy_utils::{Entry, HashMap}; -use crate::func::macros::impl_function_traits; use crate::{ - self as bevy_reflect, ApplyError, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, MaybeTyped, PartialReflect, Reflect, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, }; /// A trait used to power [map-like] operations via [reflection]. @@ -18,7 +17,7 @@ use crate::{ /// /// # Hashing /// -/// All keys are expected to return a valid hash value from [`Reflect::reflect_hash`]. +/// All keys are expected to return a valid hash value from [`PartialReflect::reflect_hash`]. /// If using the [`#[derive(Reflect)]`](derive@crate::Reflect) macro, this can be done by adding `#[reflect(Hash)]` /// to the entire struct or enum. /// This is true even for manual implementors who do not use the hashed value, @@ -27,7 +26,7 @@ use crate::{ /// # Example /// /// ``` -/// use bevy_reflect::{Reflect, Map}; +/// use bevy_reflect::{PartialReflect, Reflect, Map}; /// use bevy_utils::HashMap; /// /// @@ -35,28 +34,31 @@ use crate::{ /// foo.insert_boxed(Box::new(123_u32), Box::new(true)); /// assert_eq!(foo.len(), 1); /// -/// let field: &dyn Reflect = foo.get(&123_u32).unwrap(); -/// assert_eq!(field.downcast_ref::(), Some(&true)); +/// let field: &dyn PartialReflect = foo.get(&123_u32).unwrap(); +/// assert_eq!(field.try_downcast_ref::(), Some(&true)); /// ``` /// /// [map-like]: https://doc.rust-lang.org/book/ch08-03-hash-maps.html /// [reflection]: crate -pub trait Map: Reflect { +pub trait Map: PartialReflect { /// Returns a reference to the value associated with the given key. /// /// If no value is associated with `key`, returns `None`. - fn get(&self, key: &dyn Reflect) -> Option<&dyn Reflect>; + fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the value associated with the given key. /// /// If no value is associated with `key`, returns `None`. - fn get_mut(&mut self, key: &dyn Reflect) -> Option<&mut dyn Reflect>; + fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect>; /// Returns the key-value pair at `index` by reference, or `None` if out of bounds. - fn get_at(&self, index: usize) -> Option<(&dyn Reflect, &dyn Reflect)>; + fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)>; /// Returns the key-value pair at `index` by reference where the value is a mutable reference, or `None` if out of bounds. - fn get_at_mut(&mut self, index: usize) -> Option<(&dyn Reflect, &mut dyn Reflect)>; + fn get_at_mut( + &mut self, + index: usize, + ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)>; /// Returns the number of elements in the map. fn len(&self) -> usize; @@ -70,7 +72,7 @@ pub trait Map: Reflect { fn iter(&self) -> MapIter; /// Drain the key-value pairs of this map to get a vector of owned values. - fn drain(self: Box) -> Vec<(Box, Box)>; + fn drain(self: Box) -> Vec<(Box, Box)>; /// Clones the map, producing a [`DynamicMap`]. fn clone_dynamic(&self) -> DynamicMap; @@ -81,15 +83,15 @@ pub trait Map: Reflect { /// If the map did have this key present, the value is updated, and the old value is returned. fn insert_boxed( &mut self, - key: Box, - value: Box, - ) -> Option>; + key: Box, + value: Box, + ) -> Option>; /// Removes an entry from the map. /// /// If the map did not have this key present, `None` is returned. /// If the map did have this key present, the removed value is returned. - fn remove(&mut self, key: &dyn Reflect) -> Option>; + fn remove(&mut self, key: &dyn PartialReflect) -> Option>; } /// A container for compile-time map info. @@ -97,8 +99,10 @@ pub trait Map: Reflect { pub struct MapInfo { type_path: TypePathTable, type_id: TypeId, + key_info: fn() -> Option<&'static TypeInfo>, key_type_path: TypePathTable, key_type_id: TypeId, + value_info: fn() -> Option<&'static TypeInfo>, value_type_path: TypePathTable, value_type_id: TypeId, #[cfg(feature = "documentation")] @@ -107,13 +111,18 @@ pub struct MapInfo { impl MapInfo { /// Create a new [`MapInfo`]. - pub fn new() -> Self - { + pub fn new< + TMap: Map + TypePath, + TKey: Reflect + MaybeTyped + TypePath, + TValue: Reflect + MaybeTyped + TypePath, + >() -> Self { Self { type_path: TypePathTable::of::(), type_id: TypeId::of::(), + key_info: TKey::maybe_type_info, key_type_path: TypePathTable::of::(), key_type_id: TypeId::of::(), + value_info: TValue::maybe_type_info, value_type_path: TypePathTable::of::(), value_type_id: TypeId::of::(), #[cfg(feature = "documentation")] @@ -154,6 +163,14 @@ impl MapInfo { TypeId::of::() == self.type_id } + /// The [`TypeInfo`] of the key type. + /// + /// Returns `None` if the key type does not contain static type information, + /// such as for dynamic types. + pub fn key_info(&self) -> Option<&'static TypeInfo> { + (self.key_info)() + } + /// A representation of the type path of the key type. /// /// Provides dynamic access to all methods on [`TypePath`]. @@ -171,6 +188,14 @@ impl MapInfo { TypeId::of::() == self.key_type_id } + /// The [`TypeInfo`] of the value type. + /// + /// Returns `None` if the value type does not contain static type information, + /// such as for dynamic types. + pub fn value_info(&self) -> Option<&'static TypeInfo> { + (self.value_info)() + } + /// A representation of the type path of the value type. /// /// Provides dynamic access to all methods on [`TypePath`]. @@ -224,7 +249,7 @@ macro_rules! hash_error { #[derive(Default)] pub struct DynamicMap { represented_type: Option<&'static TypeInfo>, - values: Vec<(Box, Box)>, + values: Vec<(Box, Box)>, indices: HashMap, } @@ -249,32 +274,35 @@ impl DynamicMap { } /// Inserts a typed key-value pair into the map. - pub fn insert(&mut self, key: K, value: V) { + pub fn insert(&mut self, key: K, value: V) { self.insert_boxed(Box::new(key), Box::new(value)); } } impl Map for DynamicMap { - fn get(&self, key: &dyn Reflect) -> Option<&dyn Reflect> { + fn get(&self, key: &dyn PartialReflect) -> Option<&dyn PartialReflect> { self.indices .get(&key.reflect_hash().expect(hash_error!(key))) .map(|index| &*self.values.get(*index).unwrap().1) } - fn get_mut(&mut self, key: &dyn Reflect) -> Option<&mut dyn Reflect> { + fn get_mut(&mut self, key: &dyn PartialReflect) -> Option<&mut dyn PartialReflect> { self.indices .get(&key.reflect_hash().expect(hash_error!(key))) .cloned() .map(move |index| &mut *self.values.get_mut(index).unwrap().1) } - fn get_at(&self, index: usize) -> Option<(&dyn Reflect, &dyn Reflect)> { + fn get_at(&self, index: usize) -> Option<(&dyn PartialReflect, &dyn PartialReflect)> { self.values .get(index) .map(|(key, value)| (&**key, &**value)) } - fn get_at_mut(&mut self, index: usize) -> Option<(&dyn Reflect, &mut dyn Reflect)> { + fn get_at_mut( + &mut self, + index: usize, + ) -> Option<(&dyn PartialReflect, &mut dyn PartialReflect)> { self.values .get_mut(index) .map(|(key, value)| (&**key, &mut **value)) @@ -288,7 +316,7 @@ impl Map for DynamicMap { MapIter::new(self) } - fn drain(self: Box) -> Vec<(Box, Box)> { + fn drain(self: Box) -> Vec<(Box, Box)> { self.values } @@ -306,9 +334,9 @@ impl Map for DynamicMap { fn insert_boxed( &mut self, - key: Box, - mut value: Box, - ) -> Option> { + key: Box, + mut value: Box, + ) -> Option> { match self .indices .entry(key.reflect_hash().expect(hash_error!(key))) @@ -326,7 +354,7 @@ impl Map for DynamicMap { } } - fn remove(&mut self, key: &dyn Reflect) -> Option> { + fn remove(&mut self, key: &dyn PartialReflect) -> Option> { let index = self .indices .remove(&key.reflect_hash().expect(hash_error!(key)))?; @@ -335,52 +363,47 @@ impl Map for DynamicMap { } } -impl Reflect for DynamicMap { +impl PartialReflect for DynamicMap { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } - fn into_any(self: Box) -> Box { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn as_any(&self) -> &dyn Any { + #[inline] + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_any_mut(&mut self) -> &mut dyn Any { + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } - fn apply(&mut self, value: &dyn Reflect) { + fn apply(&mut self, value: &dyn PartialReflect) { map_apply(self, value); } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { map_try_apply(self, value) } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - fn reflect_kind(&self) -> ReflectKind { ReflectKind::Map } @@ -397,11 +420,11 @@ impl Reflect for DynamicMap { ReflectOwned::Map(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { map_partial_eq(self, value) } @@ -418,7 +441,6 @@ impl Reflect for DynamicMap { } impl_type_path!((in bevy_reflect) DynamicMap); -impl_function_traits!(DynamicMap); impl Debug for DynamicMap { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -441,7 +463,7 @@ impl<'a> MapIter<'a> { } impl<'a> Iterator for MapIter<'a> { - type Item = (&'a dyn Reflect, &'a dyn Reflect); + type Item = (&'a dyn PartialReflect, &'a dyn PartialReflect); fn next(&mut self) -> Option { let value = self.map.get_at(self.index); @@ -455,8 +477,30 @@ impl<'a> Iterator for MapIter<'a> { } } +impl FromIterator<(Box, Box)> for DynamicMap { + fn from_iter, Box)>>( + items: I, + ) -> Self { + let mut map = Self::default(); + for (key, value) in items.into_iter() { + map.insert_boxed(key, value); + } + map + } +} + +impl FromIterator<(K, V)> for DynamicMap { + fn from_iter>(items: I) -> Self { + let mut map = Self::default(); + for (key, value) in items.into_iter() { + map.insert(key, value); + } + map + } +} + impl IntoIterator for DynamicMap { - type Item = (Box, Box); + type Item = (Box, Box); type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { @@ -464,19 +508,28 @@ impl IntoIterator for DynamicMap { } } +impl<'a> IntoIterator for &'a DynamicMap { + type Item = (&'a dyn PartialReflect, &'a dyn PartialReflect); + type IntoIter = MapIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + impl<'a> ExactSizeIterator for MapIter<'a> {} -/// Compares a [`Map`] with a [`Reflect`] value. +/// Compares a [`Map`] with a [`PartialReflect`] value. /// /// Returns true if and only if all of the following are true: /// - `b` is a map; /// - `b` is the same length as `a`; /// - For each key-value pair in `a`, `b` contains a value for the given key, -/// and [`Reflect::reflect_partial_eq`] returns `Some(true)` for the two values. +/// and [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for the two values. /// /// Returns [`None`] if the comparison couldn't even be performed. #[inline] -pub fn map_partial_eq(a: &M, b: &dyn Reflect) -> Option { +pub fn map_partial_eq(a: &M, b: &dyn PartialReflect) -> Option { let ReflectRef::Map(map) = b.reflect_ref() else { return Some(false); }; @@ -533,7 +586,7 @@ pub fn map_debug(dyn_map: &dyn Map, f: &mut Formatter<'_>) -> std::fmt::Result { /// /// This function panics if `b` is not a reflected map. #[inline] -pub fn map_apply(a: &mut M, b: &dyn Reflect) { +pub fn map_apply(a: &mut M, b: &dyn PartialReflect) { if let Err(err) = map_try_apply(a, b) { panic!("{err}"); } @@ -549,7 +602,7 @@ pub fn map_apply(a: &mut M, b: &dyn Reflect) { /// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a reflected map or if /// applying elements to each other fails. #[inline] -pub fn map_try_apply(a: &mut M, b: &dyn Reflect) -> Result<(), ApplyError> { +pub fn map_try_apply(a: &mut M, b: &dyn PartialReflect) -> Result<(), ApplyError> { if let ReflectRef::Map(map_value) = b.reflect_ref() { for (key, b_value) in map_value.iter() { if let Some(a_value) = a.get_mut(key) { @@ -571,7 +624,6 @@ pub fn map_try_apply(a: &mut M, b: &dyn Reflect) -> Result<(), ApplyErro mod tests { use super::DynamicMap; use super::Map; - use crate::reflect::Reflect; #[test] fn test_into_iter() { @@ -583,10 +635,13 @@ mod tests { map.insert(2usize, expected[2].to_string()); for (index, item) in map.into_iter().enumerate() { - let key = item.0.take::().expect("couldn't downcast to usize"); + let key = item + .0 + .try_take::() + .expect("couldn't downcast to usize"); let value = item .1 - .take::() + .try_take::() .expect("couldn't downcast to String"); assert_eq!(index, key); assert_eq!(expected[index], value); @@ -603,16 +658,16 @@ mod tests { let (key_r, value_r) = map.get_at(1).expect("Item wasn't found"); let value = value_r - .downcast_ref::() + .try_downcast_ref::() .expect("Couldn't downcast to String"); let key = key_r - .downcast_ref::() + .try_downcast_ref::() .expect("Couldn't downcast to usize"); assert_eq!(key, &1usize); assert_eq!(value, &values[2].to_owned()); assert!(map.get_at(2).is_none()); - map.remove(&1usize as &dyn Reflect); + map.remove(&1usize); assert!(map.get_at(1).is_none()); } @@ -626,10 +681,10 @@ mod tests { let (key_r, value_r) = map.get_at_mut(1).expect("Item wasn't found"); let value = value_r - .downcast_mut::() + .try_downcast_mut::() .expect("Couldn't downcast to String"); let key = key_r - .downcast_ref::() + .try_downcast_ref::() .expect("Couldn't downcast to usize"); assert_eq!(key, &1usize); assert_eq!(value, &mut values[2].to_owned()); @@ -637,9 +692,9 @@ mod tests { value.clone_from(&values[0].to_owned()); assert_eq!( - map.get(&1usize as &dyn Reflect) + map.get(&1usize) .expect("Item wasn't found") - .downcast_ref::() + .try_downcast_ref::() .expect("Couldn't downcast to String"), &values[0].to_owned() ); diff --git a/crates/bevy_reflect/src/path/access.rs b/crates/bevy_reflect/src/path/access.rs index ebeeade160808d..4648b9dfa63497 100644 --- a/crates/bevy_reflect/src/path/access.rs +++ b/crates/bevy_reflect/src/path/access.rs @@ -3,14 +3,14 @@ use std::{borrow::Cow, fmt}; use super::error::AccessErrorKind; -use crate::{AccessError, Reflect, ReflectKind, ReflectMut, ReflectRef, VariantType}; +use crate::{AccessError, PartialReflect, ReflectKind, ReflectMut, ReflectRef, VariantType}; type InnerResult = Result; /// A singular element access within a path. /// Multiple accesses can be combined into a [`ParsedPath`](super::ParsedPath). /// -/// Can be applied to a [`dyn Reflect`](Reflect) to get a reference to the targeted element. +/// Can be applied to a [`dyn Reflect`](crate::Reflect) to get a reference to the targeted element. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Access<'a> { /// A name-based field access on a struct. @@ -51,15 +51,18 @@ impl<'a> Access<'a> { pub(super) fn element<'r>( &self, - base: &'r dyn Reflect, + base: &'r dyn PartialReflect, offset: Option, - ) -> Result<&'r dyn Reflect, AccessError<'a>> { + ) -> Result<&'r dyn PartialReflect, AccessError<'a>> { self.element_inner(base) .and_then(|opt| opt.ok_or(AccessErrorKind::MissingField(base.reflect_kind()))) .map_err(|err| err.with_access(self.clone(), offset)) } - fn element_inner<'r>(&self, base: &'r dyn Reflect) -> InnerResult> { + fn element_inner<'r>( + &self, + base: &'r dyn PartialReflect, + ) -> InnerResult> { use ReflectRef::*; let invalid_variant = @@ -105,9 +108,9 @@ impl<'a> Access<'a> { pub(super) fn element_mut<'r>( &self, - base: &'r mut dyn Reflect, + base: &'r mut dyn PartialReflect, offset: Option, - ) -> Result<&'r mut dyn Reflect, AccessError<'a>> { + ) -> Result<&'r mut dyn PartialReflect, AccessError<'a>> { let kind = base.reflect_kind(); self.element_inner_mut(base) @@ -117,8 +120,8 @@ impl<'a> Access<'a> { fn element_inner_mut<'r>( &self, - base: &'r mut dyn Reflect, - ) -> InnerResult> { + base: &'r mut dyn PartialReflect, + ) -> InnerResult> { use ReflectMut::*; let invalid_variant = diff --git a/crates/bevy_reflect/src/path/error.rs b/crates/bevy_reflect/src/path/error.rs index 2c6a06e772912e..d25d1670492f6c 100644 --- a/crates/bevy_reflect/src/path/error.rs +++ b/crates/bevy_reflect/src/path/error.rs @@ -74,7 +74,7 @@ impl<'a> AccessError<'a> { self.offset.as_ref() } } -impl std::fmt::Display for AccessError<'_> { +impl fmt::Display for AccessError<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let AccessError { kind, diff --git a/crates/bevy_reflect/src/path/mod.rs b/crates/bevy_reflect/src/path/mod.rs index 6a38faad595950..c3d8ccfa2de21b 100644 --- a/crates/bevy_reflect/src/path/mod.rs +++ b/crates/bevy_reflect/src/path/mod.rs @@ -8,7 +8,7 @@ mod parse; pub use parse::ParseError; use parse::PathParser; -use crate::Reflect; +use crate::{PartialReflect, Reflect}; use std::fmt; use thiserror::Error; @@ -49,19 +49,22 @@ pub trait ReflectPath<'a>: Sized { /// /// See [`GetPath::reflect_path`] for more details, /// see [`element`](Self::element) if you want a typed return value. - fn reflect_element(self, root: &dyn Reflect) -> PathResult<'a, &dyn Reflect>; + fn reflect_element(self, root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect>; /// Gets a mutable reference to the specified element on the given [`Reflect`] object. /// /// See [`GetPath::reflect_path_mut`] for more details. - fn reflect_element_mut(self, root: &mut dyn Reflect) -> PathResult<'a, &mut dyn Reflect>; + fn reflect_element_mut( + self, + root: &mut dyn PartialReflect, + ) -> PathResult<'a, &mut dyn PartialReflect>; /// Gets a `&T` to the specified element on the given [`Reflect`] object. /// /// See [`GetPath::path`] for more details. - fn element(self, root: &dyn Reflect) -> PathResult<'a, &T> { + fn element(self, root: &dyn PartialReflect) -> PathResult<'a, &T> { self.reflect_element(root).and_then(|p| { - p.downcast_ref::() + p.try_downcast_ref::() .ok_or(ReflectPathError::InvalidDowncast) }) } @@ -69,22 +72,25 @@ pub trait ReflectPath<'a>: Sized { /// Gets a `&mut T` to the specified element on the given [`Reflect`] object. /// /// See [`GetPath::path_mut`] for more details. - fn element_mut(self, root: &mut dyn Reflect) -> PathResult<'a, &mut T> { + fn element_mut(self, root: &mut dyn PartialReflect) -> PathResult<'a, &mut T> { self.reflect_element_mut(root).and_then(|p| { - p.downcast_mut::() + p.try_downcast_mut::() .ok_or(ReflectPathError::InvalidDowncast) }) } } impl<'a> ReflectPath<'a> for &'a str { - fn reflect_element(self, mut root: &dyn Reflect) -> PathResult<'a, &dyn Reflect> { + fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for (access, offset) in PathParser::new(self) { let a = access?; root = a.element(root, Some(offset))?; } Ok(root) } - fn reflect_element_mut(self, mut root: &mut dyn Reflect) -> PathResult<'a, &mut dyn Reflect> { + fn reflect_element_mut( + self, + mut root: &mut dyn PartialReflect, + ) -> PathResult<'a, &mut dyn PartialReflect> { for (access, offset) in PathParser::new(self) { root = access?.element_mut(root, Some(offset))?; } @@ -234,13 +240,13 @@ impl<'a> ReflectPath<'a> for &'a str { message = "`{Self}` does not provide a reflection path", note = "consider annotating `{Self}` with `#[derive(Reflect)]`" )] -pub trait GetPath: Reflect { +pub trait GetPath: PartialReflect { /// Returns a reference to the value specified by `path`. /// /// To retrieve a statically typed reference, use /// [`path`][GetPath::path]. - fn reflect_path<'p>(&self, path: impl ReflectPath<'p>) -> PathResult<'p, &dyn Reflect> { - path.reflect_element(self.as_reflect()) + fn reflect_path<'p>(&self, path: impl ReflectPath<'p>) -> PathResult<'p, &dyn PartialReflect> { + path.reflect_element(self.as_partial_reflect()) } /// Returns a mutable reference to the value specified by `path`. @@ -250,8 +256,8 @@ pub trait GetPath: Reflect { fn reflect_path_mut<'p>( &mut self, path: impl ReflectPath<'p>, - ) -> PathResult<'p, &mut dyn Reflect> { - path.reflect_element_mut(self.as_reflect_mut()) + ) -> PathResult<'p, &mut dyn PartialReflect> { + path.reflect_element_mut(self.as_partial_reflect_mut()) } /// Returns a statically typed reference to the value specified by `path`. @@ -262,7 +268,7 @@ pub trait GetPath: Reflect { /// /// [`DynamicStruct`]: crate::DynamicStruct fn path<'p, T: Reflect>(&self, path: impl ReflectPath<'p>) -> PathResult<'p, &T> { - path.element(self.as_reflect()) + path.element(self.as_partial_reflect()) } /// Returns a statically typed mutable reference to the value specified by `path`. @@ -273,7 +279,7 @@ pub trait GetPath: Reflect { /// /// [`DynamicStruct`]: crate::DynamicStruct fn path_mut<'p, T: Reflect>(&mut self, path: impl ReflectPath<'p>) -> PathResult<'p, &mut T> { - path.element_mut(self.as_reflect_mut()) + path.element_mut(self.as_partial_reflect_mut()) } } @@ -427,13 +433,16 @@ impl ParsedPath { } } impl<'a> ReflectPath<'a> for &'a ParsedPath { - fn reflect_element(self, mut root: &dyn Reflect) -> PathResult<'a, &dyn Reflect> { + fn reflect_element(self, mut root: &dyn PartialReflect) -> PathResult<'a, &dyn PartialReflect> { for OffsetAccess { access, offset } in &self.0 { root = access.element(root, *offset)?; } Ok(root) } - fn reflect_element_mut(self, mut root: &mut dyn Reflect) -> PathResult<'a, &mut dyn Reflect> { + fn reflect_element_mut( + self, + mut root: &mut dyn PartialReflect, + ) -> PathResult<'a, &mut dyn PartialReflect> { for OffsetAccess { access, offset } in &self.0 { root = access.element_mut(root, *offset)?; } diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 8238048d0a5b02..0456d209c7ad9b 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -1,6 +1,6 @@ use crate::{ array_debug, enum_debug, list_debug, map_debug, serde::Serializable, struct_debug, tuple_debug, - tuple_struct_debug, Array, DynamicTypePath, Enum, List, Map, Struct, Tuple, TupleStruct, + tuple_struct_debug, Array, DynamicTypePath, Enum, List, Map, Set, Struct, Tuple, TupleStruct, TypeInfo, TypePath, Typed, ValueInfo, }; use std::{ @@ -24,6 +24,7 @@ macro_rules! impl_reflect_enum { Self::List(_) => ReflectKind::List, Self::Array(_) => ReflectKind::Array, Self::Map(_) => ReflectKind::Map, + Self::Set(_) => ReflectKind::Set, Self::Enum(_) => ReflectKind::Enum, Self::Value(_) => ReflectKind::Value, } @@ -39,6 +40,7 @@ macro_rules! impl_reflect_enum { $name::List(_) => Self::List, $name::Array(_) => Self::Array, $name::Map(_) => Self::Map, + $name::Set(_) => Self::Set, $name::Enum(_) => Self::Enum, $name::Value(_) => Self::Value, } @@ -52,7 +54,7 @@ macro_rules! impl_reflect_enum { /// Each variant contains a trait object with methods specific to a kind of /// type. /// -/// A [`ReflectRef`] is obtained via [`Reflect::reflect_ref`]. +/// A [`ReflectRef`] is obtained via [`PartialReflect::reflect_ref`]. pub enum ReflectRef<'a> { Struct(&'a dyn Struct), TupleStruct(&'a dyn TupleStruct), @@ -60,8 +62,9 @@ pub enum ReflectRef<'a> { List(&'a dyn List), Array(&'a dyn Array), Map(&'a dyn Map), + Set(&'a dyn Set), Enum(&'a dyn Enum), - Value(&'a dyn Reflect), + Value(&'a dyn PartialReflect), } impl_reflect_enum!(ReflectRef<'_>); @@ -70,7 +73,7 @@ impl_reflect_enum!(ReflectRef<'_>); /// Each variant contains a trait object with methods specific to a kind of /// type. /// -/// A [`ReflectMut`] is obtained via [`Reflect::reflect_mut`]. +/// A [`ReflectMut`] is obtained via [`PartialReflect::reflect_mut`]. pub enum ReflectMut<'a> { Struct(&'a mut dyn Struct), TupleStruct(&'a mut dyn TupleStruct), @@ -78,8 +81,9 @@ pub enum ReflectMut<'a> { List(&'a mut dyn List), Array(&'a mut dyn Array), Map(&'a mut dyn Map), + Set(&'a mut dyn Set), Enum(&'a mut dyn Enum), - Value(&'a mut dyn Reflect), + Value(&'a mut dyn PartialReflect), } impl_reflect_enum!(ReflectMut<'_>); @@ -88,7 +92,7 @@ impl_reflect_enum!(ReflectMut<'_>); /// Each variant contains a trait object with methods specific to a kind of /// type. /// -/// A [`ReflectOwned`] is obtained via [`Reflect::reflect_owned`]. +/// A [`ReflectOwned`] is obtained via [`PartialReflect::reflect_owned`]. pub enum ReflectOwned { Struct(Box), TupleStruct(Box), @@ -96,12 +100,13 @@ pub enum ReflectOwned { List(Box), Array(Box), Map(Box), + Set(Box), Enum(Box), - Value(Box), + Value(Box), } impl_reflect_enum!(ReflectOwned); -/// A enumeration of all error outcomes that might happen when running [`try_apply`](Reflect::try_apply). +/// A enumeration of all error outcomes that might happen when running [`try_apply`](PartialReflect::try_apply). #[derive(Error, Debug)] pub enum ApplyError { #[error("attempted to apply `{from_kind}` to `{to_kind}`")] @@ -139,7 +144,7 @@ pub enum ApplyError { /// A zero-sized enumuration of the "kinds" of a reflected type. /// -/// A [`ReflectKind`] is obtained via [`Reflect::reflect_kind`], +/// A [`ReflectKind`] is obtained via [`PartialReflect::reflect_kind`], /// or via [`ReflectRef::kind`],[`ReflectMut::kind`] or [`ReflectOwned::kind`]. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ReflectKind { @@ -149,6 +154,7 @@ pub enum ReflectKind { List, Array, Map, + Set, Enum, Value, } @@ -162,28 +168,36 @@ impl std::fmt::Display for ReflectKind { ReflectKind::List => f.pad("list"), ReflectKind::Array => f.pad("array"), ReflectKind::Map => f.pad("map"), + ReflectKind::Set => f.pad("set"), ReflectKind::Enum => f.pad("enum"), ReflectKind::Value => f.pad("value"), } } } -/// The core trait of [`bevy_reflect`], used for accessing and modifying data dynamically. +/// The foundational trait of [`bevy_reflect`], used for accessing and modifying data dynamically. /// -/// It's recommended to use the [derive macro] rather than manually implementing this trait. -/// Doing so will automatically implement many other useful traits for reflection, +/// This is a supertrait of [`Reflect`], +/// meaning any type which implements `Reflect` implements `PartialReflect` by definition. +/// +/// It's recommended to use [the derive macro for `Reflect`] rather than manually implementing this trait. +/// Doing so will automatically implement this trait as well as many other useful traits for reflection, /// including one of the appropriate subtraits: [`Struct`], [`TupleStruct`] or [`Enum`]. /// /// See the [crate-level documentation] to see how this trait and its subtraits can be used. /// /// [`bevy_reflect`]: crate -/// [derive macro]: bevy_reflect_derive::Reflect +/// [the derive macro for `Reflect`]: bevy_reflect_derive::Reflect /// [crate-level documentation]: crate #[diagnostic::on_unimplemented( - message = "`{Self}` can not be reflected", + message = "`{Self}` does not implement `PartialReflect` so cannot be introspected", note = "consider annotating `{Self}` with `#[derive(Reflect)]`" )] -pub trait Reflect: DynamicTypePath + Any + Send + Sync { +pub trait PartialReflect: DynamicTypePath + Send + Sync +where + // NB: we don't use `Self: Any` since for downcasting, `Reflect` should be used. + Self: 'static, +{ /// Returns the [`TypeInfo`] of the type _represented_ by this value. /// /// For most types, this will simply return their own `TypeInfo`. @@ -201,27 +215,39 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// [`TypeRegistry::get_type_info`]: crate::TypeRegistry::get_type_info fn get_represented_type_info(&self) -> Option<&'static TypeInfo>; - /// Returns the value as a [`Box`][std::any::Any]. - fn into_any(self: Box) -> Box; + /// Casts this type to a boxed, reflected value. + /// + /// This is useful for coercing trait objects. + fn into_partial_reflect(self: Box) -> Box; - /// Returns the value as a [`&dyn Any`][std::any::Any]. - fn as_any(&self) -> &dyn Any; + /// Casts this type to a reflected value. + /// + /// This is useful for coercing trait objects. + fn as_partial_reflect(&self) -> &dyn PartialReflect; - /// Returns the value as a [`&mut dyn Any`][std::any::Any]. - fn as_any_mut(&mut self) -> &mut dyn Any; + /// Casts this type to a mutable, reflected value. + /// + /// This is useful for coercing trait objects. + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect; - /// Casts this type to a boxed reflected value. - fn into_reflect(self: Box) -> Box; + /// Attempts to cast this type to a boxed, [fully-reflected] value. + /// + /// [fully-reflected]: Reflect + fn try_into_reflect(self: Box) -> Result, Box>; - /// Casts this type to a reflected value. - fn as_reflect(&self) -> &dyn Reflect; + /// Attempts to cast this type to a [fully-reflected] value. + /// + /// [fully-reflected]: Reflect + fn try_as_reflect(&self) -> Option<&dyn Reflect>; - /// Casts this type to a mutable reflected value. - fn as_reflect_mut(&mut self) -> &mut dyn Reflect; + /// Attempts to cast this type to a mutable, [fully-reflected] value. + /// + /// [fully-reflected]: Reflect + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect>; /// Applies a reflected value to this value. /// - /// If a type implements a subtrait of `Reflect`, then the semantics of this + /// If a type implements an [introspection subtrait], then the semantics of this /// method are as follows: /// - If `T` is a [`Struct`], then the value of each named field of `value` is /// applied to the corresponding named field of `self`. Fields which are @@ -248,6 +274,7 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// or none of the above depending on the kind of type. For lists and maps, use the /// [`list_apply`] and [`map_apply`] helper functions when implementing this method. /// + /// [introspection subtrait]: crate#the-introspection-subtraits /// [`list_apply`]: crate::list_apply /// [`map_apply`]: crate::map_apply /// @@ -259,26 +286,20 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// - If `T` is any complex type and the corresponding fields or elements of /// `self` and `value` are not of the same type. /// - If `T` is a value type and `self` cannot be downcast to `T` - fn apply(&mut self, value: &dyn Reflect) { - Reflect::try_apply(self, value).unwrap(); + fn apply(&mut self, value: &dyn PartialReflect) { + PartialReflect::try_apply(self, value).unwrap(); } - /// Tries to [`apply`](Reflect::apply) a reflected value to this value. + /// Tries to [`apply`](PartialReflect::apply) a reflected value to this value. /// - /// Functions the same as the [`apply`](Reflect::apply) function but returns an error instead of + /// Functions the same as the [`apply`](PartialReflect::apply) function but returns an error instead of /// panicking. /// /// # Handling Errors /// /// This function may leave `self` in a partially mutated state if a error was encountered on the way. /// consider maintaining a cloned instance of this data you can switch to if a error is encountered. - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError>; - - /// Performs a type-checked assignment of a reflected value to this value. - /// - /// If `value` does not contain a value of type `T`, returns an `Err` - /// containing the trait object. - fn set(&mut self, value: Box) -> Result<(), Box>; + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError>; /// Returns a zero-sized enumeration of "kinds" of type. /// @@ -309,7 +330,7 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// or [`Enum::clone_dynamic`], respectively. /// Implementors of other `Reflect` subtraits (e.g. [`List`], [`Map`]) should /// use those subtraits' respective `clone_dynamic` methods. - fn clone_value(&self) -> Box; + fn clone_value(&self) -> Box; /// Returns a hash of the value (which includes the type). /// @@ -321,7 +342,7 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// Returns a "partial equality" comparison result. /// /// If the underlying type does not support equality testing, returns `None`. - fn reflect_partial_eq(&self, _value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, _value: &dyn PartialReflect) -> Option { None } @@ -369,29 +390,128 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { } } -impl Debug for dyn Reflect { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.debug(f) +/// A core trait of [`bevy_reflect`], used for downcasting to concrete types. +/// +/// This is a subtrait of [`PartialReflect`], +/// meaning any type which implements `Reflect` implements `PartialReflect` by definition. +/// +/// It's recommended to use [the derive macro] rather than manually implementing this trait. +/// Doing so will automatically implement this trait, [`PartialReflect`], and many other useful traits for reflection, +/// including one of the appropriate subtraits: [`Struct`], [`TupleStruct`] or [`Enum`]. +/// +/// See the [crate-level documentation] to see how this trait can be used. +/// +/// [`bevy_reflect`]: crate +/// [the derive macro]: bevy_reflect_derive::Reflect +/// [crate-level documentation]: crate +#[diagnostic::on_unimplemented( + message = "`{Self}` does not implement `Reflect` so cannot be fully reflected", + note = "consider annotating `{Self}` with `#[derive(Reflect)]`" +)] +pub trait Reflect: PartialReflect + Any { + /// Returns the value as a [`Box`][std::any::Any]. + /// + /// For remote wrapper types, this will return the remote type instead. + fn into_any(self: Box) -> Box; + + /// Returns the value as a [`&dyn Any`][std::any::Any]. + /// + /// For remote wrapper types, this will return the remote type instead. + fn as_any(&self) -> &dyn Any; + + /// Returns the value as a [`&mut dyn Any`][std::any::Any]. + /// + /// For remote wrapper types, this will return the remote type instead. + fn as_any_mut(&mut self) -> &mut dyn Any; + + /// Casts this type to a boxed, fully-reflected value. + fn into_reflect(self: Box) -> Box; + + /// Casts this type to a fully-reflected value. + fn as_reflect(&self) -> &dyn Reflect; + + /// Casts this type to a mutable, fully-reflected value. + fn as_reflect_mut(&mut self) -> &mut dyn Reflect; + + /// Performs a type-checked assignment of a reflected value to this value. + /// + /// If `value` does not contain a value of type `T`, returns an `Err` + /// containing the trait object. + fn set(&mut self, value: Box) -> Result<(), Box>; +} + +impl dyn PartialReflect { + /// Returns `true` if the underlying value represents a value of type `T`, or `false` + /// otherwise. + /// + /// Read `is` for more information on underlying values and represented types. + #[inline] + pub fn represents(&self) -> bool { + self.get_represented_type_info() + .map(|t| t.type_path() == T::type_path()) + .unwrap_or(false) + } + + /// Downcasts the value to type `T`, consuming the trait object. + /// + /// If the underlying value does not implement [`Reflect`] + /// or is not of type `T`, returns `Err(self)`. + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. + pub fn try_downcast( + self: Box, + ) -> Result, Box> { + self.try_into_reflect()? + .downcast() + .map_err(PartialReflect::into_partial_reflect) + } + + /// Downcasts the value to type `T`, unboxing and consuming the trait object. + /// + /// If the underlying value does not implement [`Reflect`] + /// or is not of type `T`, returns `Err(self)`. + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. + pub fn try_take(self: Box) -> Result> { + self.try_downcast().map(|value| *value) + } + + /// Downcasts the value to type `T` by reference. + /// + /// If the underlying value does not implement [`Reflect`] + /// or is not of type `T`, returns [`None`]. + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. + pub fn try_downcast_ref(&self) -> Option<&T> { + self.try_as_reflect()?.downcast_ref() + } + + /// Downcasts the value to type `T` by mutable reference. + /// + /// If the underlying value does not implement [`Reflect`] + /// or is not of type `T`, returns [`None`]. + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. + pub fn try_downcast_mut(&mut self) -> Option<&mut T> { + self.try_as_reflect_mut()?.downcast_mut() } } -impl Typed for dyn Reflect { - fn type_info() -> &'static TypeInfo { - static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); - CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) +impl Debug for dyn PartialReflect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.debug(f) } } // The following implementation never actually shadows the concrete TypePath implementation. - -// See this playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=589064053f27bc100d90da89c6a860aa). -impl TypePath for dyn Reflect { +// See the comment on `dyn Reflect`'s `TypePath` implementation. +impl TypePath for dyn PartialReflect { fn type_path() -> &'static str { - "dyn bevy_reflect::Reflect" + "dyn bevy_reflect::PartialReflect" } fn short_type_path() -> &'static str { - "dyn Reflect" + "dyn PartialReflect" } } @@ -400,7 +520,9 @@ impl dyn Reflect { /// Downcasts the value to type `T`, consuming the trait object. /// /// If the underlying value is not of type `T`, returns `Err(self)`. - pub fn downcast(self: Box) -> Result, Box> { + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. + pub fn downcast(self: Box) -> Result, Box> { if self.is::() { Ok(self.into_any().downcast().unwrap()) } else { @@ -411,19 +533,10 @@ impl dyn Reflect { /// Downcasts the value to type `T`, unboxing and consuming the trait object. /// /// If the underlying value is not of type `T`, returns `Err(self)`. - pub fn take(self: Box) -> Result> { - self.downcast::().map(|value| *value) - } - - /// Returns `true` if the underlying value represents a value of type `T`, or `false` - /// otherwise. /// - /// Read `is` for more information on underlying values and represented types. - #[inline] - pub fn represents(&self) -> bool { - self.get_represented_type_info() - .map(|t| t.type_path() == T::type_path()) - .unwrap_or(false) + /// For remote types, `T` should be the type itself rather than the wrapper type. + pub fn take(self: Box) -> Result> { + self.downcast::().map(|value| *value) } /// Returns `true` if the underlying value is of type `T`, or `false` @@ -435,25 +548,96 @@ impl dyn Reflect { /// to determine what type they represent. Represented types cannot be downcasted /// to, but you can use [`FromReflect`] to create a value of the represented type from them. /// + /// For remote types, `T` should be the type itself rather than the wrapper type. + /// /// [`FromReflect`]: crate::FromReflect #[inline] - pub fn is(&self) -> bool { - self.type_id() == TypeId::of::() + pub fn is(&self) -> bool { + self.as_any().type_id() == TypeId::of::() } /// Downcasts the value to type `T` by reference. /// /// If the underlying value is not of type `T`, returns `None`. + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. #[inline] - pub fn downcast_ref(&self) -> Option<&T> { + pub fn downcast_ref(&self) -> Option<&T> { self.as_any().downcast_ref::() } /// Downcasts the value to type `T` by mutable reference. /// /// If the underlying value is not of type `T`, returns `None`. + /// + /// For remote types, `T` should be the type itself rather than the wrapper type. #[inline] - pub fn downcast_mut(&mut self) -> Option<&mut T> { + pub fn downcast_mut(&mut self) -> Option<&mut T> { self.as_any_mut().downcast_mut::() } } + +impl Debug for dyn Reflect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.debug(f) + } +} + +impl Typed for dyn Reflect { + fn type_info() -> &'static TypeInfo { + static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); + CELL.get_or_set(|| TypeInfo::Value(ValueInfo::new::())) + } +} + +// The following implementation never actually shadows the concrete `TypePath` implementation. +// See this playground (https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=589064053f27bc100d90da89c6a860aa). +impl TypePath for dyn Reflect { + fn type_path() -> &'static str { + "dyn bevy_reflect::Reflect" + } + + fn short_type_path() -> &'static str { + "dyn Reflect" + } +} + +macro_rules! impl_full_reflect { + ($(<$($id:ident),* $(,)?>)? for $ty:ty $(where $($tt:tt)*)?) => { + impl $(<$($id),*>)? $crate::Reflect for $ty $(where $($tt)*)? { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn ::std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn $crate::Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn $crate::Reflect { + self + } + + fn set( + &mut self, + value: Box, + ) -> Result<(), Box> { + *self = ::take(value)?; + Ok(()) + } + } + }; +} + +pub(crate) use impl_full_reflect; diff --git a/crates/bevy_reflect/src/remote.rs b/crates/bevy_reflect/src/remote.rs new file mode 100644 index 00000000000000..a19d3bdb148d65 --- /dev/null +++ b/crates/bevy_reflect/src/remote.rs @@ -0,0 +1,64 @@ +use crate::Reflect; + +/// Marks a type as a [reflectable] wrapper for a remote type. +/// +/// This allows types from external libraries (remote types) to be included in reflection. +/// +/// # Safety +/// +/// It is highly recommended to avoid implementing this trait manually and instead use the +/// [`#[reflect_remote]`](crate::reflect_remote) attribute macro. +/// This is because the trait tends to rely on [`transmute`], which is [very unsafe]. +/// +/// The macro will ensure that the following safety requirements are met: +/// - `Self` is a single-field tuple struct (i.e. a newtype) containing the remote type. +/// - `Self` is `#[repr(transparent)]` over the remote type. +/// +/// Additionally, the macro will automatically generate [`Reflect`] and [`FromReflect`] implementations, +/// along with compile-time assertions to validate that the safety requirements have been met. +/// +/// # Example +/// +/// ``` +/// use bevy_reflect_derive::{reflect_remote, Reflect}; +/// +/// mod some_lib { +/// pub struct TheirType { +/// pub value: u32 +/// } +/// } +/// +/// #[reflect_remote(some_lib::TheirType)] +/// struct MyType { +/// pub value: u32 +/// } +/// +/// #[derive(Reflect)] +/// struct MyStruct { +/// #[reflect(remote = MyType)] +/// data: some_lib::TheirType, +/// } +/// ``` +/// +/// [reflectable]: Reflect +/// [`transmute`]: core::mem::transmute +/// [very unsafe]: https://doc.rust-lang.org/1.71.0/nomicon/transmutes.html +/// [`FromReflect`]: crate::FromReflect +pub trait ReflectRemote: Reflect { + /// The remote type this type represents via reflection. + type Remote; + + /// Converts a reference of this wrapper to a reference of its remote type. + fn as_remote(&self) -> &Self::Remote; + /// Converts a mutable reference of this wrapper to a mutable reference of its remote type. + fn as_remote_mut(&mut self) -> &mut Self::Remote; + /// Converts this wrapper into its remote type. + fn into_remote(self) -> Self::Remote; + + /// Converts a reference of the remote type to a reference of this wrapper. + fn as_wrapper(remote: &Self::Remote) -> &Self; + /// Converts a mutable reference of the remote type to a mutable reference of this wrapper. + fn as_wrapper_mut(remote: &mut Self::Remote) -> &mut Self; + /// Converts the remote type into this wrapper. + fn into_wrapper(remote: Self::Remote) -> Self; +} diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index 38dd63cd25df96..26fff47100e48a 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -1,9 +1,10 @@ use crate::serde::SerializationData; use crate::{ - ArrayInfo, DynamicArray, DynamicEnum, DynamicList, DynamicMap, DynamicStruct, DynamicTuple, - DynamicTupleStruct, DynamicVariant, EnumInfo, ListInfo, Map, MapInfo, NamedField, Reflect, - ReflectDeserialize, StructInfo, StructVariantInfo, TupleInfo, TupleStructInfo, - TupleVariantInfo, TypeInfo, TypeRegistration, TypeRegistry, VariantInfo, + ArrayInfo, DynamicArray, DynamicEnum, DynamicList, DynamicMap, DynamicSet, DynamicStruct, + DynamicTuple, DynamicTupleStruct, DynamicVariant, EnumInfo, ListInfo, Map, MapInfo, NamedField, + PartialReflect, Reflect, ReflectDeserialize, Set, SetInfo, StructInfo, StructVariantInfo, + TupleInfo, TupleStructInfo, TupleVariantInfo, TypeInfo, TypeRegistration, TypeRegistry, + VariantInfo, }; use erased_serde::Deserializer; use serde::de::{ @@ -339,21 +340,21 @@ impl<'a, 'de> DeserializeSeed<'de> for TypeRegistrationDeserializer<'a> { /// let mut deserializer = ron::Deserializer::from_str(input).unwrap(); /// let reflect_deserializer = ReflectDeserializer::new(®istry); /// -/// let output: Box = reflect_deserializer.deserialize(&mut deserializer).unwrap(); +/// let output: Box = reflect_deserializer.deserialize(&mut deserializer).unwrap(); /// /// // Since `MyStruct` is not a value type and does not register `ReflectDeserialize`, -/// // we know that its deserialized representation will be a `DynamicStruct`. -/// assert!(output.is::()); -/// assert!(output.represents::()); +/// // we know that its deserialized value will be a `DynamicStruct`, +/// // although it will represent `MyStruct`. +/// assert!(output.as_partial_reflect().represents::()); /// /// // We can convert back to `MyStruct` using `FromReflect`. -/// let value: MyStruct = ::from_reflect(&*output).unwrap(); +/// let value: MyStruct = ::from_reflect(output.as_partial_reflect()).unwrap(); /// assert_eq!(value, MyStruct { value: 123 }); /// /// // We can also do this dynamically with `ReflectFromReflect`. /// let type_id = output.get_represented_type_info().unwrap().type_id(); /// let reflect_from_reflect = registry.get_type_data::(type_id).unwrap(); -/// let value: Box = reflect_from_reflect.from_reflect(&*output).unwrap(); +/// let value: Box = reflect_from_reflect.from_reflect(output.as_partial_reflect()).unwrap(); /// assert!(value.is::()); /// assert_eq!(value.take::().unwrap(), MyStruct { value: 123 }); /// ``` @@ -378,7 +379,7 @@ impl<'a> ReflectDeserializer<'a> { } impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { - type Value = Box; + type Value = Box; fn deserialize(self, deserializer: D) -> Result where @@ -389,7 +390,7 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { } impl<'a, 'de> Visitor<'de> for UntypedReflectDeserializerVisitor<'a> { - type Value = Box; + type Value = Box; fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { formatter @@ -472,21 +473,21 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { /// let mut deserializer = ron::Deserializer::from_str(input).unwrap(); /// let reflect_deserializer = TypedReflectDeserializer::new(registration, ®istry); /// -/// let output: Box = reflect_deserializer.deserialize(&mut deserializer).unwrap(); +/// let output: Box = reflect_deserializer.deserialize(&mut deserializer).unwrap(); /// /// // Since `MyStruct` is not a value type and does not register `ReflectDeserialize`, -/// // we know that its deserialized representation will be a `DynamicStruct`. -/// assert!(output.is::()); -/// assert!(output.represents::()); +/// // we know that its deserialized value will be a `DynamicStruct`, +/// // although it will represent `MyStruct`. +/// assert!(output.as_partial_reflect().represents::()); /// /// // We can convert back to `MyStruct` using `FromReflect`. -/// let value: MyStruct = ::from_reflect(&*output).unwrap(); +/// let value: MyStruct = ::from_reflect(output.as_partial_reflect()).unwrap(); /// assert_eq!(value, MyStruct { value: 123 }); /// /// // We can also do this dynamically with `ReflectFromReflect`. /// let type_id = output.get_represented_type_info().unwrap().type_id(); /// let reflect_from_reflect = registry.get_type_data::(type_id).unwrap(); -/// let value: Box = reflect_from_reflect.from_reflect(&*output).unwrap(); +/// let value: Box = reflect_from_reflect.from_reflect(output.as_partial_reflect()).unwrap(); /// assert!(value.is::()); /// assert_eq!(value.take::().unwrap(), MyStruct { value: 123 }); /// ``` @@ -514,7 +515,7 @@ impl<'a> TypedReflectDeserializer<'a> { } impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { - type Value = Box; + type Value = Box; fn deserialize(self, deserializer: D) -> Result where @@ -525,7 +526,7 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { // Handle both Value case and types that have a custom `ReflectDeserialize` if let Some(deserialize_reflect) = self.registration.data::() { let value = deserialize_reflect.deserialize(deserializer)?; - return Ok(value); + return Ok(value.into_partial_reflect()); } match self.registration.type_info() { @@ -582,6 +583,14 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { dynamic_map.set_represented_type(Some(self.registration.type_info())); Ok(Box::new(dynamic_map)) } + TypeInfo::Set(set_info) => { + let mut dynamic_set = deserializer.deserialize_seq(SetVisitor { + set_info, + registry: self.registry, + })?; + dynamic_set.set_represented_type(Some(self.registration.type_info())); + Ok(Box::new(dynamic_set)) + } TypeInfo::Tuple(tuple_info) => { let mut dynamic_tuple = deserializer.deserialize_tuple( tuple_info.field_len(), @@ -817,6 +826,39 @@ impl<'a, 'de> Visitor<'de> for MapVisitor<'a> { } } +struct SetVisitor<'a> { + set_info: &'static SetInfo, + registry: &'a TypeRegistry, +} + +impl<'a, 'de> Visitor<'de> for SetVisitor<'a> { + type Value = DynamicSet; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("reflected set value") + } + + fn visit_seq(self, mut set: V) -> Result + where + V: SeqAccess<'de>, + { + let mut dynamic_set = DynamicSet::default(); + let value_registration = get_registration( + self.set_info.value_type_id(), + self.set_info.value_type_path_table().path(), + self.registry, + )?; + while let Some(value) = set.next_element_seed(TypedReflectDeserializer { + registration: value_registration, + registry: self.registry, + })? { + dynamic_set.insert_boxed(value); + } + + Ok(dynamic_set) + } +} + struct EnumVisitor<'a> { enum_info: &'static EnumInfo, registration: &'a TypeRegistration, @@ -1066,7 +1108,10 @@ where let Some(field) = info.field_at(*skipped_index) else { continue; }; - dynamic_struct.insert_boxed(field.name(), skipped_field.generate_default()); + dynamic_struct.insert_boxed( + field.name(), + skipped_field.generate_default().into_partial_reflect(), + ); } } @@ -1096,7 +1141,7 @@ where for index in 0..len { if let Some(value) = serialization_data.and_then(|data| data.generate_default(index)) { - tuple.insert_boxed(value); + tuple.insert_boxed(value.into_partial_reflect()); continue; } @@ -1141,7 +1186,7 @@ where .unwrap_or_default() { if let Some(value) = serialization_data.unwrap().generate_default(index) { - dynamic_struct.insert_boxed(name, value); + dynamic_struct.insert_boxed(name, value.into_partial_reflect()); } continue; } @@ -1179,11 +1224,13 @@ mod tests { use serde::de::DeserializeSeed; use serde::Deserialize; - use bevy_utils::HashMap; + use bevy_utils::{HashMap, HashSet}; use crate as bevy_reflect; use crate::serde::{ReflectDeserializer, ReflectSerializer, TypedReflectDeserializer}; - use crate::{DynamicEnum, FromReflect, Reflect, ReflectDeserialize, TypeRegistry}; + use crate::{ + DynamicEnum, FromReflect, PartialReflect, Reflect, ReflectDeserialize, TypeRegistry, + }; #[derive(Reflect, Debug, PartialEq)] struct MyStruct { @@ -1194,6 +1241,7 @@ mod tests { list_value: Vec, array_value: [i32; 5], map_value: HashMap, + set_value: HashSet, struct_value: SomeStruct, tuple_struct_value: SomeTupleStruct, unit_struct: SomeUnitStruct, @@ -1284,6 +1332,7 @@ mod tests { registry.register::<[i32; 5]>(); registry.register::>(); registry.register::>(); + registry.register::>(); registry.register::>(); registry.register::>(); registry.register_type_data::, ReflectDeserialize>(); @@ -1294,6 +1343,9 @@ mod tests { let mut map = HashMap::new(); map.insert(64, 32); + let mut set = HashSet::new(); + set.insert(64); + MyStruct { primitive_value: 123, option_value: Some(String::from("Hello world!")), @@ -1302,6 +1354,7 @@ mod tests { list_value: vec![-2, -1, 0, 1, 2], array_value: [-2, -1, 0, 1, 2], map_value: map, + set_value: set, struct_value: SomeStruct { foo: 999999999 }, tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), unit_struct: SomeUnitStruct, @@ -1348,6 +1401,9 @@ mod tests { map_value: { 64: 32, }, + set_value: [ + 64, + ], struct_value: ( foo: 999999999, ), @@ -1395,7 +1451,7 @@ mod tests { .deserialize(&mut ron_deserializer) .unwrap(); let output = dynamic_output - .take::() + .try_take::() .expect("underlying type should be f32"); assert_eq!(1.23, output); } @@ -1422,7 +1478,9 @@ mod tests { .deserialize(&mut ron_deserializer) .unwrap(); - let output = ::from_reflect(dynamic_output.as_ref()).unwrap(); + let output = + ::from_reflect(dynamic_output.as_ref().as_partial_reflect()) + .unwrap(); assert_eq!(expected, output); } @@ -1535,7 +1593,9 @@ mod tests { let output = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let expected = DynamicEnum::from(MyEnum::Tuple(1.23, 3.21)); - assert!(expected.reflect_partial_eq(output.as_ref()).unwrap()); + assert!(expected + .reflect_partial_eq(output.as_partial_reflect()) + .unwrap()); // === Struct Variant === // let input = r#"{ @@ -1550,7 +1610,9 @@ mod tests { let expected = DynamicEnum::from(MyEnum::Struct { value: String::from("I <3 Enums"), }); - assert!(expected.reflect_partial_eq(output.as_ref()).unwrap()); + assert!(expected + .reflect_partial_eq(output.as_partial_reflect()) + .unwrap()); } // Regression test for https://github.com/bevyengine/bevy/issues/12462 @@ -1566,7 +1628,7 @@ mod tests { let reflect_deserializer = ReflectDeserializer::new(®istry); let input2 = reflect_deserializer.deserialize(&mut deserializer).unwrap(); - let serializer2 = ReflectSerializer::new(&*input2, ®istry); + let serializer2 = ReflectSerializer::new(input2.as_partial_reflect(), ®istry); let serialized2 = ron::ser::to_string(&serializer2).unwrap(); assert_eq!(serialized1, serialized2); @@ -1585,12 +1647,12 @@ mod tests { 0, 0, 219, 15, 73, 64, 57, 5, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 254, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 64, 32, 0, - 0, 0, 0, 0, 0, 0, 255, 201, 154, 59, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 84, 117, 112, - 108, 101, 32, 83, 116, 114, 117, 99, 116, 0, 0, 0, 0, 1, 0, 0, 0, 123, 0, 0, 0, 0, 0, - 0, 0, 2, 0, 0, 0, 164, 112, 157, 63, 164, 112, 77, 64, 3, 0, 0, 0, 20, 0, 0, 0, 0, 0, - 0, 0, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, 97, 110, 116, 32, 118, 97, - 108, 117, 101, 1, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, - 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 64, 255, 201, 154, 59, 0, 0, 0, 0, 12, 0, 0, + 0, 0, 0, 0, 0, 84, 117, 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 0, 0, 0, 0, 1, + 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 164, 112, 157, 63, 164, 112, 77, 64, 3, + 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, + 97, 110, 116, 32, 118, 97, 108, 117, 101, 1, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, + 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, ]; let deserializer = ReflectDeserializer::new(®istry); @@ -1612,15 +1674,15 @@ mod tests { let input = vec![ 129, 217, 40, 98, 101, 118, 121, 95, 114, 101, 102, 108, 101, 99, 116, 58, 58, 115, 101, 114, 100, 101, 58, 58, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, - 83, 116, 114, 117, 99, 116, 220, 0, 19, 123, 172, 72, 101, 108, 108, 111, 32, 119, 111, + 83, 116, 114, 117, 99, 116, 220, 0, 20, 123, 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 145, 123, 146, 202, 64, 73, 15, 219, 205, 5, 57, 149, 254, 255, 0, - 1, 2, 149, 254, 255, 0, 1, 2, 129, 64, 32, 145, 206, 59, 154, 201, 255, 145, 172, 84, - 117, 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 144, 164, 85, 110, 105, 116, 129, - 167, 78, 101, 119, 84, 121, 112, 101, 123, 129, 165, 84, 117, 112, 108, 101, 146, 202, - 63, 157, 112, 164, 202, 64, 77, 112, 164, 129, 166, 83, 116, 114, 117, 99, 116, 145, - 180, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, 97, 110, 116, 32, 118, 97, 108, - 117, 101, 144, 144, 129, 166, 83, 116, 114, 117, 99, 116, 144, 129, 165, 84, 117, 112, - 108, 101, 144, 146, 100, 145, 101, + 1, 2, 149, 254, 255, 0, 1, 2, 129, 64, 32, 145, 64, 145, 206, 59, 154, 201, 255, 145, + 172, 84, 117, 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 144, 164, 85, 110, 105, + 116, 129, 167, 78, 101, 119, 84, 121, 112, 101, 123, 129, 165, 84, 117, 112, 108, 101, + 146, 202, 63, 157, 112, 164, 202, 64, 77, 112, 164, 129, 166, 83, 116, 114, 117, 99, + 116, 145, 180, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, 97, 110, 116, 32, + 118, 97, 108, 117, 101, 144, 144, 129, 166, 83, 116, 114, 117, 99, 116, 144, 129, 165, + 84, 117, 112, 108, 101, 144, 146, 100, 145, 101, ]; let mut reader = std::io::BufReader::new(input.as_slice()); diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index 0f9833c57efe4c..a962c36bd04bd1 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -8,7 +8,7 @@ pub use type_data::*; #[cfg(test)] mod tests { - use crate::{self as bevy_reflect, DynamicTupleStruct, Struct}; + use crate::{self as bevy_reflect, DynamicTupleStruct, PartialReflect, Struct}; use crate::{ serde::{ReflectDeserializer, ReflectSerializer}, type_registry::TypeRegistry, @@ -53,8 +53,7 @@ mod tests { let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap(); let reflect_deserializer = ReflectDeserializer::new(®istry); - let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); - let deserialized = value.take::().unwrap(); + let deserialized = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let mut expected = DynamicStruct::default(); expected.insert("a", 3); @@ -64,7 +63,9 @@ mod tests { expected.insert("e", 7); assert!( - expected.reflect_partial_eq(&deserialized).unwrap(), + expected + .reflect_partial_eq(deserialized.as_partial_reflect()) + .unwrap(), "Deserialization failed: expected {expected:?} found {deserialized:?}" ); @@ -75,7 +76,8 @@ mod tests { d: -1, e: 7, }; - let received = ::from_reflect(&deserialized).unwrap(); + let received = + ::from_reflect(deserialized.as_partial_reflect()).unwrap(); assert_eq!( expected, received, @@ -112,8 +114,7 @@ mod tests { let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap(); let reflect_deserializer = ReflectDeserializer::new(®istry); - let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); - let deserialized = value.take::().unwrap(); + let deserialized = reflect_deserializer.deserialize(&mut deserializer).unwrap(); let mut expected = DynamicTupleStruct::default(); expected.insert(3); @@ -123,12 +124,15 @@ mod tests { expected.insert(7); assert!( - expected.reflect_partial_eq(&deserialized).unwrap(), + expected + .reflect_partial_eq(deserialized.as_partial_reflect()) + .unwrap(), "Deserialization failed: expected {expected:?} found {deserialized:?}" ); let expected = TestStruct(3, 0, 0, -1, 7); - let received = ::from_reflect(&deserialized).unwrap(); + let received = + ::from_reflect(deserialized.as_partial_reflect()).unwrap(); assert_eq!( expected, received, @@ -173,12 +177,10 @@ mod tests { let reflect_deserializer = ReflectDeserializer::new(®istry); let expected = value.clone_value(); - let result = reflect_deserializer - .deserialize(&mut deserializer) - .unwrap() - .take::() - .unwrap(); + let result = reflect_deserializer.deserialize(&mut deserializer).unwrap(); - assert!(expected.reflect_partial_eq(&result).unwrap()); + assert!(expected + .reflect_partial_eq(result.as_partial_reflect()) + .unwrap()); } } diff --git a/crates/bevy_reflect/src/serde/ser.rs b/crates/bevy_reflect/src/serde/ser.rs index f862d0139e7fb7..86151712380358 100644 --- a/crates/bevy_reflect/src/serde/ser.rs +++ b/crates/bevy_reflect/src/serde/ser.rs @@ -1,6 +1,6 @@ use crate::{ - Array, Enum, List, Map, Reflect, ReflectRef, ReflectSerialize, Struct, Tuple, TupleStruct, - TypeInfo, TypeRegistry, VariantInfo, VariantType, + Array, Enum, List, Map, PartialReflect, ReflectRef, ReflectSerialize, Set, Struct, Tuple, + TupleStruct, TypeInfo, TypeRegistry, VariantInfo, VariantType, }; use serde::ser::{ Error, SerializeStruct, SerializeStructVariant, SerializeTuple, SerializeTupleStruct, @@ -29,9 +29,15 @@ impl<'a> Serializable<'a> { } fn get_serializable<'a, E: Error>( - reflect_value: &'a dyn Reflect, + reflect_value: &'a dyn PartialReflect, type_registry: &TypeRegistry, ) -> Result, E> { + let Some(reflect_value) = reflect_value.try_as_reflect() else { + return Err(Error::custom(format_args!( + "Type '{}' does not implement `Reflect`", + reflect_value.reflect_type_path() + ))); + }; let info = reflect_value.get_represented_type_info().ok_or_else(|| { Error::custom(format_args!( "Type '{}' does not represent any type", @@ -93,12 +99,12 @@ fn get_serializable<'a, E: Error>( /// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [type path]: crate::TypePath::type_path pub struct ReflectSerializer<'a> { - pub value: &'a dyn Reflect, + pub value: &'a dyn PartialReflect, pub registry: &'a TypeRegistry, } impl<'a> ReflectSerializer<'a> { - pub fn new(value: &'a dyn Reflect, registry: &'a TypeRegistry) -> Self { + pub fn new(value: &'a dyn PartialReflect, registry: &'a TypeRegistry) -> Self { ReflectSerializer { value, registry } } } @@ -171,12 +177,12 @@ impl<'a> Serialize for ReflectSerializer<'a> { /// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer /// [type path]: crate::TypePath::type_path pub struct TypedReflectSerializer<'a> { - pub value: &'a dyn Reflect, + pub value: &'a dyn PartialReflect, pub registry: &'a TypeRegistry, } impl<'a> TypedReflectSerializer<'a> { - pub fn new(value: &'a dyn Reflect, registry: &'a TypeRegistry) -> Self { + pub fn new(value: &'a dyn PartialReflect, registry: &'a TypeRegistry) -> Self { TypedReflectSerializer { value, registry } } } @@ -223,6 +229,11 @@ impl<'a> Serialize for TypedReflectSerializer<'a> { registry: self.registry, } .serialize(serializer), + ReflectRef::Set(value) => SetSerializer { + set: value, + registry: self.registry, + } + .serialize(serializer), ReflectRef::Enum(value) => EnumSerializer { enum_value: value, registry: self.registry, @@ -235,7 +246,7 @@ impl<'a> Serialize for TypedReflectSerializer<'a> { pub struct ReflectValueSerializer<'a> { pub registry: &'a TypeRegistry, - pub value: &'a dyn Reflect, + pub value: &'a dyn PartialReflect, } impl<'a> Serialize for ReflectValueSerializer<'a> { @@ -503,6 +514,24 @@ impl<'a> Serialize for MapSerializer<'a> { } } +pub struct SetSerializer<'a> { + pub set: &'a dyn Set, + pub registry: &'a TypeRegistry, +} + +impl<'a> Serialize for SetSerializer<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut state = serializer.serialize_seq(Some(self.set.len()))?; + for value in self.set.iter() { + state.serialize_element(&TypedReflectSerializer::new(value, self.registry))?; + } + state.end() + } +} + pub struct ListSerializer<'a> { pub list: &'a dyn List, pub registry: &'a TypeRegistry, @@ -542,9 +571,9 @@ impl<'a> Serialize for ArraySerializer<'a> { #[cfg(test)] mod tests { use crate::serde::ReflectSerializer; - use crate::{self as bevy_reflect, Struct}; + use crate::{self as bevy_reflect, PartialReflect, Struct}; use crate::{Reflect, ReflectSerialize, TypeRegistry}; - use bevy_utils::HashMap; + use bevy_utils::{HashMap, HashSet}; use ron::extensions::Extensions; use ron::ser::PrettyConfig; use serde::Serialize; @@ -560,6 +589,7 @@ mod tests { list_value: Vec, array_value: [i32; 5], map_value: HashMap, + set_value: HashSet, struct_value: SomeStruct, tuple_struct_value: SomeTupleStruct, unit_struct: SomeUnitStruct, @@ -649,6 +679,10 @@ mod tests { fn get_my_struct() -> MyStruct { let mut map = HashMap::new(); map.insert(64, 32); + + let mut set = HashSet::new(); + set.insert(64); + MyStruct { primitive_value: 123, option_value: Some(String::from("Hello world!")), @@ -657,6 +691,7 @@ mod tests { list_value: vec![-2, -1, 0, 1, 2], array_value: [-2, -1, 0, 1, 2], map_value: map, + set_value: set, struct_value: SomeStruct { foo: 999999999 }, tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), unit_struct: SomeUnitStruct, @@ -710,6 +745,9 @@ mod tests { map_value: { 64: 32, }, + set_value: [ + 64, + ], struct_value: ( foo: 999999999, ), @@ -865,12 +903,12 @@ mod tests { 0, 0, 0, 0, 219, 15, 73, 64, 57, 5, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 254, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 254, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 64, 32, - 0, 0, 0, 0, 0, 0, 0, 255, 201, 154, 59, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 84, 117, - 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 0, 0, 0, 0, 1, 0, 0, 0, 123, 0, 0, 0, 0, - 0, 0, 0, 2, 0, 0, 0, 164, 112, 157, 63, 164, 112, 77, 64, 3, 0, 0, 0, 20, 0, 0, 0, 0, - 0, 0, 0, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, 97, 110, 116, 32, 118, 97, - 108, 117, 101, 1, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, 101, 0, 0, 0, 0, 0, 0, - 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 64, 255, 201, 154, 59, 0, 0, 0, 0, 12, 0, + 0, 0, 0, 0, 0, 0, 84, 117, 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 0, 0, 0, 0, + 1, 0, 0, 0, 123, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 164, 112, 157, 63, 164, 112, 77, 64, + 3, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, + 97, 110, 116, 32, 118, 97, 108, 117, 101, 1, 0, 0, 0, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, + 0, 0, 101, 0, 0, 0, 0, 0, 0, 0, ]; assert_eq!(expected, bytes); @@ -887,15 +925,15 @@ mod tests { let expected: Vec = vec![ 129, 217, 41, 98, 101, 118, 121, 95, 114, 101, 102, 108, 101, 99, 116, 58, 58, 115, 101, 114, 100, 101, 58, 58, 115, 101, 114, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, - 121, 83, 116, 114, 117, 99, 116, 220, 0, 19, 123, 172, 72, 101, 108, 108, 111, 32, 119, + 121, 83, 116, 114, 117, 99, 116, 220, 0, 20, 123, 172, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, 145, 123, 146, 202, 64, 73, 15, 219, 205, 5, 57, 149, 254, 255, - 0, 1, 2, 149, 254, 255, 0, 1, 2, 129, 64, 32, 145, 206, 59, 154, 201, 255, 145, 172, - 84, 117, 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 144, 164, 85, 110, 105, 116, - 129, 167, 78, 101, 119, 84, 121, 112, 101, 123, 129, 165, 84, 117, 112, 108, 101, 146, - 202, 63, 157, 112, 164, 202, 64, 77, 112, 164, 129, 166, 83, 116, 114, 117, 99, 116, - 145, 180, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, 97, 110, 116, 32, 118, 97, - 108, 117, 101, 144, 144, 129, 166, 83, 116, 114, 117, 99, 116, 144, 129, 165, 84, 117, - 112, 108, 101, 144, 146, 100, 145, 101, + 0, 1, 2, 149, 254, 255, 0, 1, 2, 129, 64, 32, 145, 64, 145, 206, 59, 154, 201, 255, + 145, 172, 84, 117, 112, 108, 101, 32, 83, 116, 114, 117, 99, 116, 144, 164, 85, 110, + 105, 116, 129, 167, 78, 101, 119, 84, 121, 112, 101, 123, 129, 165, 84, 117, 112, 108, + 101, 146, 202, 63, 157, 112, 164, 202, 64, 77, 112, 164, 129, 166, 83, 116, 114, 117, + 99, 116, 145, 180, 83, 116, 114, 117, 99, 116, 32, 118, 97, 114, 105, 97, 110, 116, 32, + 118, 97, 108, 117, 101, 144, 144, 129, 166, 83, 116, 114, 117, 99, 116, 144, 129, 165, + 84, 117, 112, 108, 101, 144, 146, 100, 145, 101, ]; assert_eq!(expected, bytes); @@ -914,7 +952,7 @@ mod tests { none: None, }; let dynamic = value.clone_dynamic(); - let reflect = dynamic.as_reflect(); + let reflect = dynamic.as_partial_reflect(); let registry = get_registry(); diff --git a/crates/bevy_reflect/src/set.rs b/crates/bevy_reflect/src/set.rs new file mode 100644 index 00000000000000..08bcf4c163eced --- /dev/null +++ b/crates/bevy_reflect/src/set.rs @@ -0,0 +1,554 @@ +use std::any::{Any, TypeId}; +use std::fmt::{Debug, Formatter}; + +use bevy_reflect_derive::impl_type_path; +use bevy_utils::hashbrown::hash_table::OccupiedEntry as HashTableOccupiedEntry; +use bevy_utils::hashbrown::HashTable; + +use crate::{ + self as bevy_reflect, hash_error, ApplyError, PartialReflect, Reflect, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, +}; + +/// A trait used to power [set-like] operations via [reflection]. +/// +/// Sets contain zero or more entries of a fixed type, and correspond to types like [`HashSet`](std::collections::HashSet). The +/// order of these entries is not guaranteed by this trait. +/// +/// # Hashing +/// +/// All values are expected to return a valid hash value from [`PartialReflect::reflect_hash`]. +/// If using the [`#[derive(Reflect)]`](derive@crate::Reflect) macro, this can be done by adding `#[reflect(Hash)]` +/// to the entire struct or enum. +/// This is true even for manual implementors who do not use the hashed value, +/// as it is still relied on by [`DynamicSet`]. +/// +/// # Example +/// +/// ``` +/// use bevy_reflect::{PartialReflect, Set}; +/// use bevy_utils::HashSet; +/// +/// +/// let foo: &mut dyn Set = &mut HashSet::::new(); +/// foo.insert_boxed(Box::new(123_u32)); +/// assert_eq!(foo.len(), 1); +/// +/// let field: &dyn PartialReflect = foo.get(&123_u32).unwrap(); +/// assert_eq!(field.try_downcast_ref::(), Some(&123_u32)); +/// ``` +/// +/// [set-like]: https://doc.rust-lang.org/stable/std/collections/struct.HashSet.html +/// [reflection]: crate +pub trait Set: PartialReflect { + /// Returns a reference to the value. + /// + /// If no value is contained, returns `None`. + fn get(&self, value: &dyn PartialReflect) -> Option<&dyn PartialReflect>; + + /// Returns the number of elements in the set. + fn len(&self) -> usize; + + /// Returns `true` if the list contains no elements. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns an iterator over the values of the set. + fn iter(&self) -> Box + '_>; + + /// Drain the values of this set to get a vector of owned values. + fn drain(self: Box) -> Vec>; + + /// Clones the set, producing a [`DynamicSet`]. + fn clone_dynamic(&self) -> DynamicSet; + + /// Inserts a value into the set. + /// + /// If the set did not have this value present, `true` is returned. + /// If the set did have this value present, `false` is returned. + fn insert_boxed(&mut self, value: Box) -> bool; + + /// Removes a value from the set. + /// + /// If the set did not have this value present, `true` is returned. + /// If the set did have this value present, `false` is returned. + fn remove(&mut self, value: &dyn PartialReflect) -> bool; + + /// Checks if the given value is contained in the set + fn contains(&self, value: &dyn PartialReflect) -> bool; +} + +/// A container for compile-time set info. +#[derive(Clone, Debug)] +pub struct SetInfo { + type_path: TypePathTable, + type_id: TypeId, + value_type_path: TypePathTable, + value_type_id: TypeId, + #[cfg(feature = "documentation")] + docs: Option<&'static str>, +} + +impl SetInfo { + /// Create a new [`SetInfo`]. + pub fn new() -> Self { + Self { + type_path: TypePathTable::of::(), + type_id: TypeId::of::(), + value_type_path: TypePathTable::of::(), + value_type_id: TypeId::of::(), + #[cfg(feature = "documentation")] + docs: None, + } + } + + /// Sets the docstring for this set. + #[cfg(feature = "documentation")] + pub fn with_docs(self, docs: Option<&'static str>) -> Self { + Self { docs, ..self } + } + + /// A representation of the type path of the set. + /// + /// Provides dynamic access to all methods on [`TypePath`]. + pub fn type_path_table(&self) -> &TypePathTable { + &self.type_path + } + + /// The [stable, full type path] of the set. + /// + /// Use [`type_path_table`] if you need access to the other methods on [`TypePath`]. + /// + /// [stable, full type path]: TypePath + /// [`type_path_table`]: Self::type_path_table + pub fn type_path(&self) -> &'static str { + self.type_path_table().path() + } + + /// The [`TypeId`] of the set. + pub fn type_id(&self) -> TypeId { + self.type_id + } + + /// Check if the given type matches the set type. + pub fn is(&self) -> bool { + TypeId::of::() == self.type_id + } + + /// A representation of the type path of the value type. + /// + /// Provides dynamic access to all methods on [`TypePath`]. + pub fn value_type_path_table(&self) -> &TypePathTable { + &self.value_type_path + } + + /// The [`TypeId`] of the value. + pub fn value_type_id(&self) -> TypeId { + self.value_type_id + } + + /// Check if the given type matches the value type. + pub fn value_is(&self) -> bool { + TypeId::of::() == self.value_type_id + } + + /// The docstring of this set, if any. + #[cfg(feature = "documentation")] + pub fn docs(&self) -> Option<&'static str> { + self.docs + } +} + +/// An ordered set of reflected values. +#[derive(Default)] +pub struct DynamicSet { + represented_type: Option<&'static TypeInfo>, + hash_table: HashTable>, +} + +impl DynamicSet { + /// Sets the [type] to be represented by this `DynamicSet`. + /// + /// # Panics + /// + /// Panics if the given [type] is not a [`TypeInfo::Set`]. + /// + /// [type]: TypeInfo + pub fn set_represented_type(&mut self, represented_type: Option<&'static TypeInfo>) { + if let Some(represented_type) = represented_type { + assert!( + matches!(represented_type, TypeInfo::Set(_)), + "expected TypeInfo::Set but received: {:?}", + represented_type + ); + } + + self.represented_type = represented_type; + } + + /// Inserts a typed value into the set. + pub fn insert(&mut self, value: V) { + self.insert_boxed(Box::new(value)); + } + + fn internal_hash(value: &dyn PartialReflect) -> u64 { + value.reflect_hash().expect(hash_error!(value)) + } + + fn internal_eq( + value: &dyn PartialReflect, + ) -> impl FnMut(&Box) -> bool + '_ { + |other| { + value + .reflect_partial_eq(&**other) + .expect("Underlying type does not reflect `PartialEq` and hence doesn't support equality checks") + } + } +} + +impl Set for DynamicSet { + fn get(&self, value: &dyn PartialReflect) -> Option<&dyn PartialReflect> { + self.hash_table + .find(Self::internal_hash(value), Self::internal_eq(value)) + .map(|value| &**value) + } + + fn len(&self) -> usize { + self.hash_table.len() + } + + fn iter(&self) -> Box + '_> { + let iter = self.hash_table.iter().map(|v| &**v); + Box::new(iter) + } + + fn drain(self: Box) -> Vec> { + self.hash_table.into_iter().collect::>() + } + + fn clone_dynamic(&self) -> DynamicSet { + let mut hash_table = HashTable::new(); + self.hash_table + .iter() + .map(|value| value.clone_value()) + .for_each(|value| { + hash_table.insert_unique(Self::internal_hash(value.as_ref()), value, |boxed| { + Self::internal_hash(boxed.as_ref()) + }); + }); + + DynamicSet { + represented_type: self.represented_type, + hash_table, + } + } + + fn insert_boxed(&mut self, value: Box) -> bool { + assert_eq!( + value.reflect_partial_eq(&*value), + Some(true), + "Values inserted in `Set` like types are expected to reflect `PartialEq`" + ); + match self + .hash_table + .find_mut(Self::internal_hash(&*value), Self::internal_eq(&*value)) + { + Some(old) => { + *old = value; + false + } + None => { + self.hash_table.insert_unique( + Self::internal_hash(value.as_ref()), + value, + |boxed| Self::internal_hash(boxed.as_ref()), + ); + true + } + } + } + + fn remove(&mut self, value: &dyn PartialReflect) -> bool { + self.hash_table + .find_entry(Self::internal_hash(value), Self::internal_eq(value)) + .map(HashTableOccupiedEntry::remove) + .is_ok() + } + + fn contains(&self, value: &dyn PartialReflect) -> bool { + self.hash_table + .find(Self::internal_hash(value), Self::internal_eq(value)) + .is_some() + } +} + +impl PartialReflect for DynamicSet { + #[inline] + fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { + self.represented_type + } + + #[inline] + fn into_partial_reflect(self: Box) -> Box { + self + } + + #[inline] + fn as_partial_reflect(&self) -> &dyn PartialReflect { + self + } + + #[inline] + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { + self + } + + #[inline] + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) + } + + #[inline] + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None + } + + #[inline] + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None + } + + fn apply(&mut self, value: &dyn PartialReflect) { + set_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + set_try_apply(self, value) + } + + fn reflect_kind(&self) -> ReflectKind { + ReflectKind::Set + } + + fn reflect_ref(&self) -> ReflectRef { + ReflectRef::Set(self) + } + + fn reflect_mut(&mut self) -> ReflectMut { + ReflectMut::Set(self) + } + + fn reflect_owned(self: Box) -> ReflectOwned { + ReflectOwned::Set(self) + } + + fn clone_value(&self) -> Box { + Box::new(self.clone_dynamic()) + } + + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { + set_partial_eq(self, value) + } + + fn debug(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "DynamicSet(")?; + set_debug(self, f)?; + write!(f, ")") + } + + #[inline] + fn is_dynamic(&self) -> bool { + true + } +} + +impl_type_path!((in bevy_reflect) DynamicSet); + +impl Debug for DynamicSet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.debug(f) + } +} + +impl FromIterator> for DynamicSet { + fn from_iter>>(values: I) -> Self { + let mut this = Self { + represented_type: None, + hash_table: HashTable::new(), + }; + + for value in values { + this.insert_boxed(value); + } + + this + } +} + +impl FromIterator for DynamicSet { + fn from_iter>(values: I) -> Self { + let mut this = Self { + represented_type: None, + hash_table: HashTable::new(), + }; + + for value in values { + this.insert(value); + } + + this + } +} + +impl IntoIterator for DynamicSet { + type Item = Box; + type IntoIter = bevy_utils::hashbrown::hash_table::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.hash_table.into_iter() + } +} + +impl<'a> IntoIterator for &'a DynamicSet { + type Item = &'a dyn PartialReflect; + type IntoIter = std::iter::Map< + bevy_utils::hashbrown::hash_table::Iter<'a, Box>, + fn(&'a Box) -> Self::Item, + >; + + fn into_iter(self) -> Self::IntoIter { + self.hash_table.iter().map(|v| v.as_ref()) + } +} + +/// Compares a [`Set`] with a [`PartialReflect`] value. +/// +/// Returns true if and only if all of the following are true: +/// - `b` is a set; +/// - `b` is the same length as `a`; +/// - For each value pair in `a`, `b` contains the value too, +/// and [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for the two values. +/// +/// Returns [`None`] if the comparison couldn't even be performed. +#[inline] +pub fn set_partial_eq(a: &M, b: &dyn PartialReflect) -> Option { + let ReflectRef::Set(set) = b.reflect_ref() else { + return Some(false); + }; + + if a.len() != set.len() { + return Some(false); + } + + for value in a.iter() { + if let Some(set_value) = set.get(value) { + let eq_result = value.reflect_partial_eq(set_value); + if let failed @ (Some(false) | None) = eq_result { + return failed; + } + } else { + return Some(false); + } + } + + Some(true) +} + +/// The default debug formatter for [`Set`] types. +/// +/// # Example +/// ``` +/// # use bevy_utils::HashSet; +/// use bevy_reflect::Reflect; +/// +/// let mut my_set = HashSet::new(); +/// my_set.insert(String::from("Hello")); +/// println!("{:#?}", &my_set as &dyn Reflect); +/// +/// // Output: +/// +/// // { +/// // "Hello", +/// // } +/// ``` +#[inline] +pub fn set_debug(dyn_set: &dyn Set, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut debug = f.debug_set(); + for value in dyn_set.iter() { + debug.entry(&value as &dyn Debug); + } + debug.finish() +} + +/// Applies the elements of reflected set `b` to the corresponding elements of set `a`. +/// +/// If a value from `b` does not exist in `a`, the value is cloned and inserted. +/// +/// # Panics +/// +/// This function panics if `b` is not a reflected set. +#[inline] +pub fn set_apply(a: &mut M, b: &dyn PartialReflect) { + if let ReflectRef::Set(set_value) = b.reflect_ref() { + for b_value in set_value.iter() { + if a.get(b_value).is_none() { + a.insert_boxed(b_value.clone_value()); + } + } + } else { + panic!("Attempted to apply a non-set type to a set type."); + } +} + +/// Tries to apply the elements of reflected set `b` to the corresponding elements of set `a` +/// and returns a Result. +/// +/// If a key from `b` does not exist in `a`, the value is cloned and inserted. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a reflected set or if +/// applying elements to each other fails. +#[inline] +pub fn set_try_apply(a: &mut S, b: &dyn PartialReflect) -> Result<(), ApplyError> { + if let ReflectRef::Set(set_value) = b.reflect_ref() { + for b_value in set_value.iter() { + if a.get(b_value).is_none() { + a.insert_boxed(b_value.clone_value()); + } + } + } else { + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::Set, + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::DynamicSet; + + #[test] + fn test_into_iter() { + let expected = ["foo", "bar", "baz"]; + + let mut set = DynamicSet::default(); + set.insert(expected[0].to_string()); + set.insert(expected[1].to_string()); + set.insert(expected[2].to_string()); + + for item in set.into_iter() { + let value = item + .try_take::() + .expect("couldn't downcast to String"); + let index = expected + .iter() + .position(|i| *i == value.as_str()) + .expect("Element found in expected array"); + assert_eq!(expected[index], value); + } + } +} diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 92cba76baa6aac..52dce37f91b945 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -1,8 +1,7 @@ use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; -use crate::func::macros::impl_function_traits; use crate::{ - self as bevy_reflect, ApplyError, NamedField, Reflect, ReflectKind, ReflectMut, ReflectOwned, - ReflectRef, TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, NamedField, PartialReflect, Reflect, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, }; use bevy_reflect_derive::impl_type_path; use bevy_utils::HashMap; @@ -26,7 +25,7 @@ use std::{ /// # Example /// /// ``` -/// use bevy_reflect::{Reflect, Struct}; +/// use bevy_reflect::{PartialReflect, Reflect, Struct}; /// /// #[derive(Reflect)] /// struct Foo { @@ -38,30 +37,30 @@ use std::{ /// assert_eq!(foo.field_len(), 1); /// assert_eq!(foo.name_at(0), Some("bar")); /// -/// let field: &dyn Reflect = foo.field("bar").unwrap(); -/// assert_eq!(field.downcast_ref::(), Some(&123)); +/// let field: &dyn PartialReflect = foo.field("bar").unwrap(); +/// assert_eq!(field.try_downcast_ref::(), Some(&123)); /// ``` /// /// [struct-like]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html /// [reflection]: crate /// [unit structs]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#unit-like-structs-without-any-fields -pub trait Struct: Reflect { +pub trait Struct: PartialReflect { /// Returns a reference to the value of the field named `name` as a `&dyn - /// Reflect`. - fn field(&self, name: &str) -> Option<&dyn Reflect>; + /// PartialReflect`. + fn field(&self, name: &str) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the value of the field named `name` as a - /// `&mut dyn Reflect`. - fn field_mut(&mut self, name: &str) -> Option<&mut dyn Reflect>; + /// `&mut dyn PartialReflect`. + fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect>; /// Returns a reference to the value of the field with index `index` as a - /// `&dyn Reflect`. - fn field_at(&self, index: usize) -> Option<&dyn Reflect>; + /// `&dyn PartialReflect`. + fn field_at(&self, index: usize) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the value of the field with index `index` - /// as a `&mut dyn Reflect`. - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn Reflect>; + /// as a `&mut dyn PartialReflect`. + fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect>; /// Returns the name of the field with index `index`. fn name_at(&self, index: usize) -> Option<&str>; @@ -215,7 +214,7 @@ impl<'a> FieldIter<'a> { } impl<'a> Iterator for FieldIter<'a> { - type Item = &'a dyn Reflect; + type Item = &'a dyn PartialReflect; fn next(&mut self) -> Option { let value = self.struct_val.field_at(self.index); @@ -263,23 +262,25 @@ pub trait GetField { impl GetField for S { fn get_field(&self, name: &str) -> Option<&T> { - self.field(name).and_then(|value| value.downcast_ref::()) + self.field(name) + .and_then(|value| value.try_downcast_ref::()) } fn get_field_mut(&mut self, name: &str) -> Option<&mut T> { self.field_mut(name) - .and_then(|value| value.downcast_mut::()) + .and_then(|value| value.try_downcast_mut::()) } } impl GetField for dyn Struct { fn get_field(&self, name: &str) -> Option<&T> { - self.field(name).and_then(|value| value.downcast_ref::()) + self.field(name) + .and_then(|value| value.try_downcast_ref::()) } fn get_field_mut(&mut self, name: &str) -> Option<&mut T> { self.field_mut(name) - .and_then(|value| value.downcast_mut::()) + .and_then(|value| value.try_downcast_mut::()) } } @@ -287,7 +288,7 @@ impl GetField for dyn Struct { #[derive(Default)] pub struct DynamicStruct { represented_type: Option<&'static TypeInfo>, - fields: Vec>, + fields: Vec>, field_names: Vec>, field_indices: HashMap, usize>, } @@ -315,7 +316,11 @@ impl DynamicStruct { /// Inserts a field named `name` with value `value` into the struct. /// /// If the field already exists, it is overwritten. - pub fn insert_boxed<'a>(&mut self, name: impl Into>, value: Box) { + pub fn insert_boxed<'a>( + &mut self, + name: impl Into>, + value: Box, + ) { let name: Cow = name.into(); if let Some(index) = self.field_indices.get(&name) { self.fields[*index] = value; @@ -330,7 +335,7 @@ impl DynamicStruct { /// Inserts a field named `name` with the typed value `value` into the struct. /// /// If the field already exists, it is overwritten. - pub fn insert<'a, T: Reflect>(&mut self, name: impl Into>, value: T) { + pub fn insert<'a, T: PartialReflect>(&mut self, name: impl Into>, value: T) { self.insert_boxed(name, Box::new(value)); } @@ -342,14 +347,14 @@ impl DynamicStruct { impl Struct for DynamicStruct { #[inline] - fn field(&self, name: &str) -> Option<&dyn Reflect> { + fn field(&self, name: &str) -> Option<&dyn PartialReflect> { self.field_indices .get(name) .map(|index| &*self.fields[*index]) } #[inline] - fn field_mut(&mut self, name: &str) -> Option<&mut dyn Reflect> { + fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect> { if let Some(index) = self.field_indices.get(name) { Some(&mut *self.fields[*index]) } else { @@ -358,12 +363,12 @@ impl Struct for DynamicStruct { } #[inline] - fn field_at(&self, index: usize) -> Option<&dyn Reflect> { + fn field_at(&self, index: usize) -> Option<&dyn PartialReflect> { self.fields.get(index).map(|value| &**value) } #[inline] - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { self.fields.get_mut(index).map(|value| &mut **value) } @@ -399,43 +404,38 @@ impl Struct for DynamicStruct { } } -impl Reflect for DynamicStruct { +impl PartialReflect for DynamicStruct { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { 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(); @@ -452,12 +452,6 @@ impl Reflect for DynamicStruct { Ok(()) } - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - #[inline] fn reflect_kind(&self) -> ReflectKind { ReflectKind::Struct @@ -479,11 +473,11 @@ impl Reflect for DynamicStruct { } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { struct_partial_eq(self, value) } @@ -500,7 +494,6 @@ impl Reflect for DynamicStruct { } impl_type_path!((in bevy_reflect) DynamicStruct); -impl_function_traits!(DynamicStruct); impl Debug for DynamicStruct { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -508,17 +501,50 @@ impl Debug for DynamicStruct { } } -/// Compares a [`Struct`] with a [`Reflect`] value. +impl<'a, N> FromIterator<(N, Box)> for DynamicStruct +where + N: Into>, +{ + /// Create a dynamic struct that doesn't represent a type from the + /// field name, field value pairs. + fn from_iter)>>(fields: I) -> Self { + let mut dynamic_struct = Self::default(); + for (name, value) in fields.into_iter() { + dynamic_struct.insert_boxed(name, value); + } + dynamic_struct + } +} + +impl IntoIterator for DynamicStruct { + type Item = Box; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.fields.into_iter() + } +} + +impl<'a> IntoIterator for &'a DynamicStruct { + type Item = &'a dyn PartialReflect; + type IntoIter = FieldIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_fields() + } +} + +/// Compares a [`Struct`] with a [`PartialReflect`] value. /// /// Returns true if and only if all of the following are true: /// - `b` is a struct; /// - For each field in `a`, `b` contains a field with the same name and -/// [`Reflect::reflect_partial_eq`] returns `Some(true)` for the two field +/// [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for the two field /// values. /// /// Returns [`None`] if the comparison couldn't even be performed. #[inline] -pub fn struct_partial_eq(a: &S, b: &dyn Reflect) -> Option { +pub fn struct_partial_eq(a: &S, b: &dyn PartialReflect) -> Option { let ReflectRef::Struct(struct_value) = b.reflect_ref() else { return Some(false); }; diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 4e96d50f20351d..b63a5f6c777e83 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -3,10 +3,10 @@ use bevy_utils::all_tuples; use crate::{ self as bevy_reflect, utility::GenericTypePathCell, ApplyError, FromReflect, - GetTypeRegistration, Reflect, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, - TypeRegistration, TypeRegistry, Typed, UnnamedField, + GetTypeRegistration, MaybeTyped, Reflect, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, + TypePath, TypeRegistration, TypeRegistry, Typed, UnnamedField, }; -use crate::{ReflectKind, TypePathTable}; +use crate::{PartialReflect, ReflectKind, TypePathTable}; use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; use std::slice::Iter; @@ -22,25 +22,25 @@ use std::slice::Iter; /// # Example /// /// ``` -/// use bevy_reflect::{Reflect, Tuple}; +/// use bevy_reflect::{PartialReflect, Tuple}; /// /// let foo = (123_u32, true); /// assert_eq!(foo.field_len(), 2); /// -/// let field: &dyn Reflect = foo.field(0).unwrap(); -/// assert_eq!(field.downcast_ref::(), Some(&123)); +/// let field: &dyn PartialReflect = foo.field(0).unwrap(); +/// assert_eq!(field.try_downcast_ref::(), Some(&123)); /// ``` /// /// [tuple-like]: https://doc.rust-lang.org/book/ch03-02-data-types.html#the-tuple-type /// [reflection]: crate -pub trait Tuple: Reflect { +pub trait Tuple: PartialReflect { /// Returns a reference to the value of the field with index `index` as a /// `&dyn Reflect`. - fn field(&self, index: usize) -> Option<&dyn Reflect>; + fn field(&self, index: usize) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the value of the field with index `index` /// as a `&mut dyn Reflect`. - fn field_mut(&mut self, index: usize) -> Option<&mut dyn Reflect>; + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect>; /// Returns the number of fields in the tuple. fn field_len(&self) -> usize; @@ -49,7 +49,7 @@ pub trait Tuple: Reflect { fn iter_fields(&self) -> TupleFieldIter; /// Drain the fields of this tuple to get a vector of owned values. - fn drain(self: Box) -> Vec>; + fn drain(self: Box) -> Vec>; /// Clones the struct into a [`DynamicTuple`]. fn clone_dynamic(&self) -> DynamicTuple; @@ -71,7 +71,7 @@ impl<'a> TupleFieldIter<'a> { } impl<'a> Iterator for TupleFieldIter<'a> { - type Item = &'a dyn Reflect; + type Item = &'a dyn PartialReflect; fn next(&mut self) -> Option { let value = self.tuple.field(self.index); @@ -115,24 +115,24 @@ pub trait GetTupleField { impl GetTupleField for S { fn get_field(&self, index: usize) -> Option<&T> { self.field(index) - .and_then(|value| value.downcast_ref::()) + .and_then(|value| value.try_downcast_ref::()) } fn get_field_mut(&mut self, index: usize) -> Option<&mut T> { self.field_mut(index) - .and_then(|value| value.downcast_mut::()) + .and_then(|value| value.try_downcast_mut::()) } } impl GetTupleField for dyn Tuple { fn get_field(&self, index: usize) -> Option<&T> { self.field(index) - .and_then(|value| value.downcast_ref::()) + .and_then(|value| value.try_downcast_ref::()) } fn get_field_mut(&mut self, index: usize) -> Option<&mut T> { self.field_mut(index) - .and_then(|value| value.downcast_mut::()) + .and_then(|value| value.try_downcast_mut::()) } } @@ -222,7 +222,7 @@ impl TupleInfo { #[derive(Default, Debug)] pub struct DynamicTuple { represented_type: Option<&'static TypeInfo>, - fields: Vec>, + fields: Vec>, } impl DynamicTuple { @@ -245,13 +245,13 @@ impl DynamicTuple { } /// Appends an element with value `value` to the tuple. - pub fn insert_boxed(&mut self, value: Box) { + pub fn insert_boxed(&mut self, value: Box) { self.represented_type = None; self.fields.push(value); } /// Appends a typed element with value `value` to the tuple. - pub fn insert(&mut self, value: T) { + pub fn insert(&mut self, value: T) { self.represented_type = None; self.insert_boxed(Box::new(value)); } @@ -259,12 +259,12 @@ impl DynamicTuple { impl Tuple for DynamicTuple { #[inline] - fn field(&self, index: usize) -> Option<&dyn Reflect> { + fn field(&self, index: usize) -> Option<&dyn PartialReflect> { self.fields.get(index).map(|field| &**field) } #[inline] - fn field_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { self.fields.get_mut(index).map(|field| &mut **field) } @@ -282,7 +282,7 @@ impl Tuple for DynamicTuple { } #[inline] - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { self.fields } @@ -299,51 +299,41 @@ impl Tuple for DynamicTuple { } } -impl Reflect for DynamicTuple { +impl PartialReflect for DynamicTuple { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } - #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } - fn apply(&mut self, value: &dyn Reflect) { + fn apply(&mut self, value: &dyn PartialReflect) { tuple_apply(self, value); } - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - #[inline] fn reflect_kind(&self) -> ReflectKind { ReflectKind::Tuple @@ -365,15 +355,15 @@ impl Reflect for DynamicTuple { } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { tuple_try_apply(self, value) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { tuple_partial_eq(self, value) } @@ -391,13 +381,40 @@ impl Reflect for DynamicTuple { impl_type_path!((in bevy_reflect) DynamicTuple); +impl FromIterator> for DynamicTuple { + fn from_iter>>(fields: I) -> Self { + Self { + represented_type: None, + fields: fields.into_iter().collect(), + } + } +} + +impl IntoIterator for DynamicTuple { + type Item = Box; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.fields.into_iter() + } +} + +impl<'a> IntoIterator for &'a DynamicTuple { + type Item = &'a dyn PartialReflect; + type IntoIter = TupleFieldIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_fields() + } +} + /// Applies the elements of `b` to the corresponding elements of `a`. /// /// # Panics /// /// This function panics if `b` is not a tuple. #[inline] -pub fn tuple_apply(a: &mut T, b: &dyn Reflect) { +pub fn tuple_apply(a: &mut T, b: &dyn PartialReflect) { if let Err(err) = tuple_try_apply(a, b) { panic!("{err}"); } @@ -411,7 +428,7 @@ pub fn tuple_apply(a: &mut T, b: &dyn Reflect) { /// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a tuple or if /// applying elements to each other fails. #[inline] -pub fn tuple_try_apply(a: &mut T, b: &dyn Reflect) -> Result<(), ApplyError> { +pub fn tuple_try_apply(a: &mut T, b: &dyn PartialReflect) -> Result<(), ApplyError> { if let ReflectRef::Tuple(tuple) = b.reflect_ref() { for (i, value) in tuple.iter_fields().enumerate() { if let Some(v) = a.field_mut(i) { @@ -427,16 +444,16 @@ pub fn tuple_try_apply(a: &mut T, b: &dyn Reflect) -> Result<(), Apply Ok(()) } -/// Compares a [`Tuple`] with a [`Reflect`] value. +/// Compares a [`Tuple`] with a [`PartialReflect`] value. /// /// Returns true if and only if all of the following are true: /// - `b` is a tuple; /// - `b` has the same number of elements as `a`; -/// - [`Reflect::reflect_partial_eq`] returns `Some(true)` for pairwise elements of `a` and `b`. +/// - [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for pairwise elements of `a` and `b`. /// /// Returns [`None`] if the comparison couldn't even be performed. #[inline] -pub fn tuple_partial_eq(a: &T, b: &dyn Reflect) -> Option { +pub fn tuple_partial_eq(a: &T, b: &dyn PartialReflect) -> Option { let ReflectRef::Tuple(b) = b.reflect_ref() else { return Some(false); }; @@ -483,19 +500,19 @@ pub fn tuple_debug(dyn_tuple: &dyn Tuple, f: &mut Formatter<'_>) -> std::fmt::Re macro_rules! impl_reflect_tuple { {$($index:tt : $name:tt),*} => { - impl<$($name: Reflect + TypePath + GetTypeRegistration),*> Tuple for ($($name,)*) { + impl<$($name: Reflect + MaybeTyped + TypePath + GetTypeRegistration),*> Tuple for ($($name,)*) { #[inline] - fn field(&self, index: usize) -> Option<&dyn Reflect> { + fn field(&self, index: usize) -> Option<&dyn PartialReflect> { match index { - $($index => Some(&self.$index as &dyn Reflect),)* + $($index => Some(&self.$index as &dyn PartialReflect),)* _ => None, } } #[inline] - fn field_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { match index { - $($index => Some(&mut self.$index as &mut dyn Reflect),)* + $($index => Some(&mut self.$index as &mut dyn PartialReflect),)* _ => None, } } @@ -515,7 +532,7 @@ macro_rules! impl_reflect_tuple { } #[inline] - fn drain(self: Box) -> Vec> { + fn drain(self: Box) -> Vec> { vec![ $(Box::new(self.$index),)* ] @@ -534,46 +551,34 @@ macro_rules! impl_reflect_tuple { } } - impl<$($name: Reflect + TypePath + GetTypeRegistration),*> Reflect for ($($name,)*) { + impl<$($name: Reflect + MaybeTyped + TypePath + GetTypeRegistration),*> PartialReflect for ($($name,)*) { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) } - fn into_any(self: Box) -> Box { - self - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn Any { + #[inline] + fn into_partial_reflect(self: Box) -> Box { self } - fn into_reflect(self: Box) -> Box { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } - fn as_reflect(&self) -> &dyn Reflect { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Ok(self) } - fn apply(&mut self, value: &dyn Reflect) { - crate::tuple_apply(self, value); + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + Some(self) } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { - crate::tuple_try_apply(self, value) - } - - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + Some(self) } fn reflect_kind(&self) -> ReflectKind { @@ -592,16 +597,55 @@ macro_rules! impl_reflect_tuple { ReflectOwned::Tuple(self) } - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { crate::tuple_partial_eq(self, value) } + + fn apply(&mut self, value: &dyn PartialReflect) { + crate::tuple_apply(self, value); + } + + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { + crate::tuple_try_apply(self, value) + } } - impl <$($name: Reflect + TypePath + GetTypeRegistration),*> Typed for ($($name,)*) { + impl<$($name: Reflect + MaybeTyped + TypePath + GetTypeRegistration),*> Reflect for ($($name,)*) { + fn into_any(self: Box) -> Box { + self + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn into_reflect(self: Box) -> Box { + self + } + + fn as_reflect(&self) -> &dyn Reflect { + self + } + + fn as_reflect_mut(&mut self) -> &mut dyn Reflect { + self + } + + fn set(&mut self, value: Box) -> Result<(), Box> { + *self = value.take()?; + Ok(()) + } + } + + impl <$($name: Reflect + MaybeTyped + TypePath + GetTypeRegistration),*> Typed for ($($name,)*) { fn type_info() -> &'static TypeInfo { static CELL: $crate::utility::GenericTypeInfoCell = $crate::utility::GenericTypeInfoCell::new(); CELL.get_or_insert::(|| { @@ -614,7 +658,7 @@ macro_rules! impl_reflect_tuple { } } - impl<$($name: Reflect + TypePath + GetTypeRegistration),*> GetTypeRegistration for ($($name,)*) { + impl<$($name: Reflect + MaybeTyped + TypePath + GetTypeRegistration),*> GetTypeRegistration for ($($name,)*) { fn get_type_registration() -> TypeRegistration { TypeRegistration::of::<($($name,)*)>() } @@ -624,9 +668,9 @@ macro_rules! impl_reflect_tuple { } } - impl<$($name: FromReflect + TypePath + GetTypeRegistration),*> FromReflect for ($($name,)*) + impl<$($name: FromReflect + MaybeTyped + TypePath + GetTypeRegistration),*> FromReflect for ($($name,)*) { - fn from_reflect(reflect: &dyn Reflect) -> Option { + fn from_reflect(reflect: &dyn PartialReflect) -> Option { if let ReflectRef::Tuple(_ref_tuple) = reflect.reflect_ref() { Some( ( @@ -658,7 +702,7 @@ impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, 10: K, 11: L} macro_rules! impl_type_path_tuple { - () => { + ($(#[$meta:meta])*) => { impl TypePath for () { fn type_path() -> &'static str { "()" @@ -670,7 +714,8 @@ macro_rules! impl_type_path_tuple { } }; - ($param:ident) => { + ($(#[$meta:meta])* $param:ident) => { + $(#[$meta])* impl <$param: TypePath> TypePath for ($param,) { fn type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); @@ -688,8 +733,8 @@ macro_rules! impl_type_path_tuple { } }; - ($last:ident $(,$param:ident)*) => { - + ($(#[$meta:meta])* $last:ident $(,$param:ident)*) => { + $(#[$meta])* impl <$($param: TypePath,)* $last: TypePath> TypePath for ($($param,)* $last) { fn type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); @@ -708,32 +753,41 @@ macro_rules! impl_type_path_tuple { }; } -all_tuples!(impl_type_path_tuple, 0, 12, P); - -macro_rules! impl_get_ownership_tuple { +all_tuples!( + #[doc(fake_variadic)] + impl_type_path_tuple, + 0, + 12, + P +); + +#[cfg(feature = "functions")] +const _: () = { + macro_rules! impl_get_ownership_tuple { ($($name: ident),*) => { $crate::func::args::impl_get_ownership!(($($name,)*); <$($name),*>); }; } -all_tuples!(impl_get_ownership_tuple, 0, 12, P); + all_tuples!(impl_get_ownership_tuple, 0, 12, P); -macro_rules! impl_from_arg_tuple { + macro_rules! impl_from_arg_tuple { ($($name: ident),*) => { - $crate::func::args::impl_from_arg!(($($name,)*); <$($name: FromReflect + TypePath + GetTypeRegistration),*>); + $crate::func::args::impl_from_arg!(($($name,)*); <$($name: FromReflect + MaybeTyped + TypePath + GetTypeRegistration),*>); }; } -all_tuples!(impl_from_arg_tuple, 0, 12, P); + all_tuples!(impl_from_arg_tuple, 0, 12, P); -macro_rules! impl_into_return_tuple { + macro_rules! impl_into_return_tuple { ($($name: ident),+) => { - $crate::func::impl_into_return!(($($name,)*); <$($name: FromReflect + TypePath + GetTypeRegistration),*>); + $crate::func::impl_into_return!(($($name,)*); <$($name: FromReflect + MaybeTyped + TypePath + GetTypeRegistration),*>); }; } -// The unit type (i.e. `()`) is special-cased, so we skip implementing it here. -all_tuples!(impl_into_return_tuple, 1, 12, P); + // The unit type (i.e. `()`) is special-cased, so we skip implementing it here. + all_tuples!(impl_into_return_tuple, 1, 12, P); +}; #[cfg(test)] mod tests { diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index 87c87fb3760dea..dd923e47be7950 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -1,10 +1,9 @@ use bevy_reflect_derive::impl_type_path; use crate::attributes::{impl_custom_attribute_methods, CustomAttributes}; -use crate::func::macros::impl_function_traits; use crate::{ - self as bevy_reflect, ApplyError, DynamicTuple, Reflect, ReflectKind, ReflectMut, ReflectOwned, - ReflectRef, Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, + self as bevy_reflect, ApplyError, DynamicTuple, PartialReflect, Reflect, ReflectKind, + ReflectMut, ReflectOwned, ReflectRef, Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, }; use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; @@ -22,7 +21,7 @@ use std::sync::Arc; /// # Example /// /// ``` -/// use bevy_reflect::{Reflect, TupleStruct}; +/// use bevy_reflect::{PartialReflect, Reflect, TupleStruct}; /// /// #[derive(Reflect)] /// struct Foo(u32); @@ -31,20 +30,20 @@ use std::sync::Arc; /// /// assert_eq!(foo.field_len(), 1); /// -/// let field: &dyn Reflect = foo.field(0).unwrap(); -/// assert_eq!(field.downcast_ref::(), Some(&123)); +/// let field: &dyn PartialReflect = foo.field(0).unwrap(); +/// assert_eq!(field.try_downcast_ref::(), Some(&123)); /// ``` /// /// [tuple struct-like]: https://doc.rust-lang.org/book/ch05-01-defining-structs.html#using-tuple-structs-without-named-fields-to-create-different-types /// [reflection]: crate -pub trait TupleStruct: Reflect { +pub trait TupleStruct: PartialReflect { /// Returns a reference to the value of the field with index `index` as a /// `&dyn Reflect`. - fn field(&self, index: usize) -> Option<&dyn Reflect>; + fn field(&self, index: usize) -> Option<&dyn PartialReflect>; /// Returns a mutable reference to the value of the field with index `index` /// as a `&mut dyn Reflect`. - fn field_mut(&mut self, index: usize) -> Option<&mut dyn Reflect>; + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect>; /// Returns the number of fields in the tuple struct. fn field_len(&self) -> usize; @@ -166,7 +165,7 @@ impl<'a> TupleStructFieldIter<'a> { } impl<'a> Iterator for TupleStructFieldIter<'a> { - type Item = &'a dyn Reflect; + type Item = &'a dyn PartialReflect; fn next(&mut self) -> Option { let value = self.tuple_struct.field(self.index); @@ -213,24 +212,24 @@ pub trait GetTupleStructField { impl GetTupleStructField for S { fn get_field(&self, index: usize) -> Option<&T> { self.field(index) - .and_then(|value| value.downcast_ref::()) + .and_then(|value| value.try_downcast_ref::()) } fn get_field_mut(&mut self, index: usize) -> Option<&mut T> { self.field_mut(index) - .and_then(|value| value.downcast_mut::()) + .and_then(|value| value.try_downcast_mut::()) } } impl GetTupleStructField for dyn TupleStruct { fn get_field(&self, index: usize) -> Option<&T> { self.field(index) - .and_then(|value| value.downcast_ref::()) + .and_then(|value| value.try_downcast_ref::()) } fn get_field_mut(&mut self, index: usize) -> Option<&mut T> { self.field_mut(index) - .and_then(|value| value.downcast_mut::()) + .and_then(|value| value.try_downcast_mut::()) } } @@ -238,7 +237,7 @@ impl GetTupleStructField for dyn TupleStruct { #[derive(Default)] pub struct DynamicTupleStruct { represented_type: Option<&'static TypeInfo>, - fields: Vec>, + fields: Vec>, } impl DynamicTupleStruct { @@ -262,24 +261,24 @@ impl DynamicTupleStruct { } /// Appends an element with value `value` to the tuple struct. - pub fn insert_boxed(&mut self, value: Box) { + pub fn insert_boxed(&mut self, value: Box) { self.fields.push(value); } /// Appends a typed element with value `value` to the tuple struct. - pub fn insert(&mut self, value: T) { + pub fn insert(&mut self, value: T) { self.insert_boxed(Box::new(value)); } } impl TupleStruct for DynamicTupleStruct { #[inline] - fn field(&self, index: usize) -> Option<&dyn Reflect> { + fn field(&self, index: usize) -> Option<&dyn PartialReflect> { self.fields.get(index).map(|field| &**field) } #[inline] - fn field_mut(&mut self, index: usize) -> Option<&mut dyn Reflect> { + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { self.fields.get_mut(index).map(|field| &mut **field) } @@ -308,43 +307,40 @@ impl TupleStruct for DynamicTupleStruct { } } -impl Reflect for DynamicTupleStruct { +impl PartialReflect for DynamicTupleStruct { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { self.represented_type } #[inline] - fn into_any(self: Box) -> Box { + fn into_partial_reflect(self: Box) -> Box { self } #[inline] - fn as_any(&self) -> &dyn Any { + fn as_partial_reflect(&self) -> &dyn PartialReflect { self } #[inline] - fn as_any_mut(&mut self) -> &mut dyn Any { + fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { self } - #[inline] - fn into_reflect(self: Box) -> Box { - self + fn try_into_reflect(self: Box) -> Result, Box> { + Err(self) } - #[inline] - fn as_reflect(&self) -> &dyn Reflect { - self + fn try_as_reflect(&self) -> Option<&dyn Reflect> { + None } - #[inline] - fn as_reflect_mut(&mut self) -> &mut dyn Reflect { - self + fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { + None } - fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { 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) { @@ -360,12 +356,6 @@ impl Reflect for DynamicTupleStruct { Ok(()) } - #[inline] - fn set(&mut self, value: Box) -> Result<(), Box> { - *self = value.take()?; - Ok(()) - } - #[inline] fn reflect_kind(&self) -> ReflectKind { ReflectKind::TupleStruct @@ -387,12 +377,12 @@ impl Reflect for DynamicTupleStruct { } #[inline] - fn clone_value(&self) -> Box { + fn clone_value(&self) -> Box { Box::new(self.clone_dynamic()) } #[inline] - fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { + fn reflect_partial_eq(&self, value: &dyn PartialReflect) -> Option { tuple_struct_partial_eq(self, value) } @@ -409,7 +399,6 @@ impl Reflect for DynamicTupleStruct { } impl_type_path!((in bevy_reflect) DynamicTupleStruct); -impl_function_traits!(DynamicTupleStruct); impl Debug for DynamicTupleStruct { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -426,16 +415,46 @@ impl From for DynamicTupleStruct { } } -/// Compares a [`TupleStruct`] with a [`Reflect`] value. +impl FromIterator> for DynamicTupleStruct { + fn from_iter>>(fields: I) -> Self { + Self { + represented_type: None, + fields: fields.into_iter().collect(), + } + } +} + +impl IntoIterator for DynamicTupleStruct { + type Item = Box; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.fields.into_iter() + } +} + +impl<'a> IntoIterator for &'a DynamicTupleStruct { + type Item = &'a dyn PartialReflect; + type IntoIter = TupleStructFieldIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_fields() + } +} + +/// Compares a [`TupleStruct`] with a [`PartialReflect`] value. /// /// Returns true if and only if all of the following are true: /// - `b` is a tuple struct; /// - `b` has the same number of fields as `a`; -/// - [`Reflect::reflect_partial_eq`] returns `Some(true)` for pairwise fields of `a` and `b`. +/// - [`PartialReflect::reflect_partial_eq`] returns `Some(true)` for pairwise fields of `a` and `b`. /// /// Returns [`None`] if the comparison couldn't even be performed. #[inline] -pub fn tuple_struct_partial_eq(a: &S, b: &dyn Reflect) -> Option { +pub fn tuple_struct_partial_eq( + a: &S, + b: &dyn PartialReflect, +) -> Option { let ReflectRef::TupleStruct(tuple_struct) = b.reflect_ref() else { return Some(false); }; diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 1f68dbb7d0fa0a..a6a7ceb2404241 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -1,9 +1,11 @@ use crate::{ - ArrayInfo, EnumInfo, ListInfo, MapInfo, Reflect, StructInfo, TupleInfo, TupleStructInfo, - TypePath, TypePathTable, + ArrayInfo, DynamicArray, DynamicEnum, DynamicList, DynamicMap, DynamicStruct, DynamicTuple, + DynamicTupleStruct, EnumInfo, ListInfo, MapInfo, PartialReflect, Reflect, ReflectKind, SetInfo, + StructInfo, TupleInfo, TupleStructInfo, TypePath, TypePathTable, }; use std::any::{Any, TypeId}; use std::fmt::Debug; +use thiserror::Error; /// A static accessor to compile-time type information. /// @@ -25,7 +27,7 @@ use std::fmt::Debug; /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, TypeInfo, TypePath, ValueInfo, ApplyError}; +/// # use bevy_reflect::{DynamicTypePath, NamedField, PartialReflect, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, TypeInfo, TypePath, ValueInfo, ApplyError}; /// # use bevy_reflect::utility::NonGenericTypeInfoCell; /// use bevy_reflect::Typed; /// @@ -52,20 +54,28 @@ use std::fmt::Debug; /// # fn type_path() -> &'static str { todo!() } /// # fn short_type_path() -> &'static str { todo!() } /// # } +/// # impl PartialReflect for MyStruct { +/// # fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { todo!() } +/// # fn into_partial_reflect(self: Box) -> Box { todo!() } +/// # fn as_partial_reflect(&self) -> &dyn PartialReflect { todo!() } +/// # fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { todo!() } +/// # fn try_into_reflect(self: Box) -> Result, Box> { todo!() } +/// # fn try_as_reflect(&self) -> Option<&dyn Reflect> { todo!() } +/// # fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { todo!() } +/// # fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { todo!() } +/// # fn reflect_ref(&self) -> ReflectRef { todo!() } +/// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } +/// # fn reflect_owned(self: Box) -> ReflectOwned { todo!() } +/// # fn clone_value(&self) -> Box { todo!() } +/// # } /// # impl Reflect for MyStruct { -/// # fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { todo!() } -/// # fn into_any(self: Box) -> Box { todo!() } -/// # fn as_any(&self) -> &dyn Any { todo!() } -/// # fn as_any_mut(&mut self) -> &mut dyn Any { todo!() } -/// # fn into_reflect(self: Box) -> Box { todo!() } -/// # fn as_reflect(&self) -> &dyn Reflect { todo!() } -/// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } -/// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } -/// # fn reflect_ref(&self) -> ReflectRef { todo!() } -/// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } -/// # fn reflect_owned(self: Box) -> ReflectOwned { todo!() } -/// # fn clone_value(&self) -> Box { todo!() } +/// # fn into_any(self: Box) -> Box { todo!() } +/// # fn as_any(&self) -> &dyn Any { todo!() } +/// # fn as_any_mut(&mut self) -> &mut dyn Any { todo!() } +/// # fn into_reflect(self: Box) -> Box { todo!() } +/// # fn as_reflect(&self) -> &dyn Reflect { todo!() } +/// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } +/// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # } /// ``` /// @@ -81,17 +91,69 @@ pub trait Typed: Reflect + TypePath { fn type_info() -> &'static TypeInfo; } +/// A wrapper trait around [`Typed`]. +/// +/// This trait is used to provide a way to get compile-time type information for types that +/// do implement `Typed` while also allowing for types that do not implement `Typed` to be used. +/// It's used instead of `Typed` directly to avoid making dynamic types also +/// implement `Typed` in order to be used as active fields. +/// +/// This trait has a blanket implementation for all types that implement `Typed` +/// and manual implementations for all dynamic types (which simply return `None`). +#[doc(hidden)] +pub trait MaybeTyped: PartialReflect { + /// Returns the compile-time [info] for the underlying type, if it exists. + /// + /// [info]: TypeInfo + fn maybe_type_info() -> Option<&'static TypeInfo> { + None + } +} + +impl MaybeTyped for T { + fn maybe_type_info() -> Option<&'static TypeInfo> { + Some(T::type_info()) + } +} + +impl MaybeTyped for DynamicEnum {} + +impl MaybeTyped for DynamicTupleStruct {} + +impl MaybeTyped for DynamicStruct {} + +impl MaybeTyped for DynamicMap {} + +impl MaybeTyped for DynamicList {} + +impl MaybeTyped for DynamicArray {} + +impl MaybeTyped for DynamicTuple {} + +/// A [`TypeInfo`]-specific error. +#[derive(Debug, Error)] +pub enum TypeInfoError { + /// Caused when a type was expected to be of a certain [kind], but was not. + /// + /// [kind]: ReflectKind + #[error("kind mismatch: expected {expected:?}, received {received:?}")] + KindMismatch { + expected: ReflectKind, + received: ReflectKind, + }, +} + /// Compile-time type information for various reflected types. /// /// Generally, for any given type, this value can be retrieved one of three ways: /// /// 1. [`Typed::type_info`] -/// 2. [`Reflect::get_represented_type_info`] +/// 2. [`PartialReflect::get_represented_type_info`] /// 3. [`TypeRegistry::get_type_info`] /// /// Each return a static reference to [`TypeInfo`], but they all have their own use cases. /// For example, if you know the type at compile time, [`Typed::type_info`] is probably -/// the simplest. If all you have is a `dyn Reflect`, you'll probably want [`Reflect::get_represented_type_info`]. +/// the simplest. If all you have is a `dyn PartialReflect`, you'll probably want [`PartialReflect::get_represented_type_info`]. /// Lastly, if all you have is a [`TypeId`] or [type path], you will need to go through /// [`TypeRegistry::get_type_info`]. /// @@ -99,8 +161,8 @@ pub trait Typed: Reflect + TypePath { /// it can be more performant. This is because those other methods may require attaining a lock on /// the static [`TypeInfo`], while the registry simply checks a map. /// -/// [`Reflect::get_represented_type_info`]: Reflect::get_represented_type_info /// [`TypeRegistry::get_type_info`]: crate::TypeRegistry::get_type_info +/// [`PartialReflect::get_represented_type_info`]: crate::PartialReflect::get_represented_type_info /// [type path]: TypePath::type_path #[derive(Debug, Clone)] pub enum TypeInfo { @@ -110,6 +172,7 @@ pub enum TypeInfo { List(ListInfo), Array(ArrayInfo), Map(MapInfo), + Set(SetInfo), Enum(EnumInfo), Value(ValueInfo), } @@ -124,6 +187,7 @@ impl TypeInfo { Self::List(info) => info.type_id(), Self::Array(info) => info.type_id(), Self::Map(info) => info.type_id(), + Self::Set(info) => info.type_id(), Self::Enum(info) => info.type_id(), Self::Value(info) => info.type_id(), } @@ -140,6 +204,7 @@ impl TypeInfo { Self::List(info) => info.type_path_table(), Self::Array(info) => info.type_path_table(), Self::Map(info) => info.type_path_table(), + Self::Set(info) => info.type_path_table(), Self::Enum(info) => info.type_path_table(), Self::Value(info) => info.type_path_table(), } @@ -170,10 +235,56 @@ impl TypeInfo { Self::List(info) => info.docs(), Self::Array(info) => info.docs(), Self::Map(info) => info.docs(), + Self::Set(info) => info.docs(), Self::Enum(info) => info.docs(), Self::Value(info) => info.docs(), } } + + /// Returns the [kind] of this `TypeInfo`. + /// + /// [kind]: ReflectKind + pub fn kind(&self) -> ReflectKind { + match self { + Self::Struct(_) => ReflectKind::Struct, + Self::TupleStruct(_) => ReflectKind::TupleStruct, + Self::Tuple(_) => ReflectKind::Tuple, + Self::List(_) => ReflectKind::List, + Self::Array(_) => ReflectKind::Array, + Self::Map(_) => ReflectKind::Map, + Self::Set(_) => ReflectKind::Set, + Self::Enum(_) => ReflectKind::Enum, + Self::Value(_) => ReflectKind::Value, + } + } +} + +macro_rules! impl_cast_method { + ($name:ident : $kind:ident => $info:ident) => { + #[doc = concat!("Attempts a cast to [`", stringify!($info), "`].")] + #[doc = concat!("\n\nReturns an error if `self` is not [`TypeInfo::", stringify!($kind), "`].")] + pub fn $name(&self) -> Result<&$info, TypeInfoError> { + match self { + Self::$kind(info) => Ok(info), + _ => Err(TypeInfoError::KindMismatch { + expected: ReflectKind::$kind, + received: self.kind(), + }), + } + } + }; +} + +/// Conversion convenience methods for [`TypeInfo`]. +impl TypeInfo { + impl_cast_method!(as_struct: Struct => StructInfo); + impl_cast_method!(as_tuple_struct: TupleStruct => TupleStructInfo); + impl_cast_method!(as_tuple: Tuple => TupleInfo); + impl_cast_method!(as_list: List => ListInfo); + impl_cast_method!(as_array: Array => ArrayInfo); + impl_cast_method!(as_map: Map => MapInfo); + impl_cast_method!(as_enum: Enum => EnumInfo); + impl_cast_method!(as_value: Value => ValueInfo); } /// A container for compile-time info related to general value types, including primitives. @@ -241,3 +352,20 @@ impl ValueInfo { self.docs } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_return_error_on_invalid_cast() { + let info = as Typed>::type_info(); + assert!(matches!( + info.as_struct(), + Err(TypeInfoError::KindMismatch { + expected: ReflectKind::Struct, + received: ReflectKind::List + }) + )); + } +} diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index eb848d99997054..1263b0bbed5239 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -584,7 +584,7 @@ impl FromType for Reflec value .downcast_ref::() .map(|value| Serializable::Borrowed(value)) - .or_else(|| T::from_reflect(value).map(|value| Serializable::Owned(Box::new(value)))) + .or_else(|| T::from_reflect(value.as_partial_reflect()).map(|value| Serializable::Owned(Box::new(value)))) .unwrap_or_else(|| { panic!( "FromReflect::from_reflect failed when called on type `{}` with this value: {value:?}", @@ -788,7 +788,11 @@ mod test { let dyn_reflect = unsafe { reflect_from_ptr.as_reflect(Ptr::from(&value)) }; match dyn_reflect.reflect_ref() { bevy_reflect::ReflectRef::Struct(strukt) => { - let a = strukt.field("a").unwrap().downcast_ref::().unwrap(); + let a = strukt + .field("a") + .unwrap() + .try_downcast_ref::() + .unwrap(); assert_eq!(*a, 2.0); } _ => panic!("invalid reflection"), diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index 86dcbbc1754622..92f7a9a8bbfd56 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -49,7 +49,7 @@ mod sealed { /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, Typed, TypeInfo, TypePath, ApplyError}; +/// # use bevy_reflect::{DynamicTypePath, NamedField, PartialReflect, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, Typed, TypeInfo, TypePath, ApplyError}; /// use bevy_reflect::utility::NonGenericTypeInfoCell; /// /// struct Foo { @@ -70,20 +70,28 @@ mod sealed { /// # fn type_path() -> &'static str { todo!() } /// # fn short_type_path() -> &'static str { todo!() } /// # } -/// # impl Reflect for Foo { +/// # impl PartialReflect for Foo { /// # fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { todo!() } +/// # fn into_partial_reflect(self: Box) -> Box { todo!() } +/// # fn as_partial_reflect(&self) -> &dyn PartialReflect { todo!() } +/// # fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { todo!() } +/// # fn try_into_reflect(self: Box) -> Result, Box> { todo!() } +/// # fn try_as_reflect(&self) -> Option<&dyn Reflect> { todo!() } +/// # fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { todo!() } +/// # fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { todo!() } +/// # fn reflect_ref(&self) -> ReflectRef { todo!() } +/// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } +/// # fn reflect_owned(self: Box) -> ReflectOwned { todo!() } +/// # fn clone_value(&self) -> Box { todo!() } +/// # } +/// # impl Reflect for Foo { /// # fn into_any(self: Box) -> Box { todo!() } /// # fn as_any(&self) -> &dyn Any { todo!() } /// # fn as_any_mut(&mut self) -> &mut dyn Any { todo!() } /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } -/// # fn reflect_ref(&self) -> ReflectRef { todo!() } -/// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } -/// # fn reflect_owned(self: Box) -> ReflectOwned { todo!() } -/// # fn clone_value(&self) -> Box { todo!() } /// # } /// ``` /// @@ -130,12 +138,12 @@ impl Default for NonGenericTypeCell { /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, Reflect, ReflectMut, ReflectOwned, ReflectRef, TupleStructInfo, Typed, TypeInfo, TypePath, UnnamedField, ApplyError}; +/// # use bevy_reflect::{DynamicTypePath, PartialReflect, Reflect, ReflectMut, ReflectOwned, ReflectRef, TupleStructInfo, Typed, TypeInfo, TypePath, UnnamedField, ApplyError}; /// use bevy_reflect::utility::GenericTypeInfoCell; /// /// struct Foo(T); /// -/// impl Typed for Foo { +/// impl Typed for Foo { /// fn type_info() -> &'static TypeInfo { /// static CELL: GenericTypeInfoCell = GenericTypeInfoCell::new(); /// CELL.get_or_insert::(|| { @@ -149,20 +157,28 @@ impl Default for NonGenericTypeCell { /// # fn type_path() -> &'static str { todo!() } /// # fn short_type_path() -> &'static str { todo!() } /// # } -/// # impl Reflect for Foo { +/// # impl PartialReflect for Foo { /// # fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { todo!() } +/// # fn into_partial_reflect(self: Box) -> Box { todo!() } +/// # fn as_partial_reflect(&self) -> &dyn PartialReflect { todo!() } +/// # fn as_partial_reflect_mut(&mut self) -> &mut dyn PartialReflect { todo!() } +/// # fn try_into_reflect(self: Box) -> Result, Box> { todo!() } +/// # fn try_as_reflect(&self) -> Option<&dyn Reflect> { todo!() } +/// # fn try_as_reflect_mut(&mut self) -> Option<&mut dyn Reflect> { todo!() } +/// # fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { todo!() } +/// # fn reflect_ref(&self) -> ReflectRef { todo!() } +/// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } +/// # fn reflect_owned(self: Box) -> ReflectOwned { todo!() } +/// # fn clone_value(&self) -> Box { todo!() } +/// # } +/// # impl Reflect for Foo { /// # fn into_any(self: Box) -> Box { todo!() } /// # fn as_any(&self) -> &dyn Any { todo!() } /// # fn as_any_mut(&mut self) -> &mut dyn Any { todo!() } /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } -/// # fn reflect_ref(&self) -> ReflectRef { todo!() } -/// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } -/// # fn reflect_owned(self: Box) -> ReflectOwned { todo!() } -/// # fn clone_value(&self) -> Box { todo!() } /// # } /// ``` /// diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 4da6baaf9d2ad6..9ebd06678bfc3b 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -29,7 +29,7 @@ zstd = ["ruzstd"] trace = ["profiling"] tracing-tracy = [] -wgpu_trace = ["wgpu/trace"] +wgpu_trace = [] ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] @@ -58,34 +58,34 @@ bevy_render_macros = { path = "macros", version = "0.15.0-dev" } bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +bevy_winit = { path = "../bevy_winit", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } # rendering -image = { version = "0.25", default-features = false } +image = { version = "0.25.2", default-features = false } # misc codespan-reporting = "0.11.0" -# `fragile-send-sync-non-atomic-wasm` feature means we can't use WASM threads for rendering +# `fragile-send-sync-non-atomic-wasm` feature means we can't use Wasm threads for rendering # It is enabled for now to avoid having to do a significant overhaul of the renderer just for wasm. # When the 'atomics' feature is enabled `fragile-send-sync-non-atomic` does nothing # and Bevy instead wraps `wgpu` types to verify they are not used off their origin thread. -wgpu = { version = "0.20", default-features = false, features = [ +wgpu = { version = "22", default-features = false, features = [ "wgsl", "dx12", "metal", - "naga", "naga-ir", "fragile-send-sync-non-atomic-wasm", ] } -naga = { version = "0.20", features = ["wgsl-in"] } +naga = { version = "22", features = ["wgsl-in"] } serde = { version = "1", features = ["derive"] } bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5", features = ["derive", "must_cast"] } downcast-rs = "1.2.0" thiserror = "1.0" futures-lite = "2.0.1" -hexasphere = "12.0" +hexasphere = "14.0" ddsfile = { version = "0.5.2", optional = true } ktx2 = { version = "0.3.0", optional = true } # For ktx2 supercompression @@ -93,7 +93,7 @@ flate2 = { version = "1.0.22", optional = true } ruzstd = { version = "0.7.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.8", features = ["glam"] } +encase = { version = "0.9", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = [ "profile-with-tracing", @@ -101,15 +101,16 @@ profiling = { version = "1", features = [ async-channel = "2.2.0" nonmax = "0.5" smallvec = { version = "1.11", features = ["const_new"] } +offset-allocator = "0.2" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.14", default-features = false, features = [ +naga_oil = { version = "0.15", default-features = false, features = [ "test_shader", ] } [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.14" +naga_oil = "0.15" js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', @@ -129,5 +130,5 @@ send_wrapper = "0.6.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index b4a047caf444c4..fab68977bc98b2 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -22,5 +22,5 @@ quote = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index 25887495f41bf5..8fa69d29f1a068 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -355,6 +355,20 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { let fallback_image = get_fallback_image(&render_path, *dimension); + let expected_samplers = match sampler_binding_type { + SamplerBindingType::Filtering => { + quote!( [#render_path::render_resource::TextureSampleType::Float { filterable: true }] ) + } + SamplerBindingType::NonFiltering => quote!([ + #render_path::render_resource::TextureSampleType::Float { filterable: false }, + #render_path::render_resource::TextureSampleType::Sint, + #render_path::render_resource::TextureSampleType::Uint, + ]), + SamplerBindingType::Comparison => { + quote!( [#render_path::render_resource::TextureSampleType::Depth] ) + } + }; + // insert fallible texture-based entries at 0 so that if we fail here, we exit before allocating any buffers binding_impls.insert(0, quote! { ( @@ -362,7 +376,26 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { #render_path::render_resource::OwnedBindingResource::Sampler({ 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)?.sampler.clone() + let image = images.get(handle).ok_or_else(|| #render_path::render_resource::AsBindGroupError::RetryNextUpdate)?; + + let Some(sample_type) = image.texture_format.sample_type(None, None) else { + return Err(#render_path::render_resource::AsBindGroupError::InvalidSamplerType( + #binding_index, + "None".to_string(), + format!("{:?}", #expected_samplers), + )); + }; + + let valid = #expected_samplers.contains(&sample_type); + + if !valid { + return Err(#render_path::render_resource::AsBindGroupError::InvalidSamplerType( + #binding_index, + format!("{:?}", sample_type), + format!("{:?}", #expected_samplers), + )); + } + image.sampler.clone() } else { #fallback_image.sampler.clone() } diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 60794636b49e6e..b03f4ba1843adb 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -24,7 +24,7 @@ use crate::{ }, render_resource::{BufferVec, GpuArrayBufferable, RawBufferVec, UninitBufferVec}, renderer::{RenderAdapter, RenderDevice, RenderQueue}, - view::{GpuCulling, ViewTarget}, + view::{ExtractedView, GpuCulling, ViewTarget}, Render, RenderApp, RenderSet, }; @@ -185,8 +185,9 @@ pub struct IndirectParameters { /// This field is in the same place in both structures. pub instance_count: u32, - /// The index of the first vertex we're to draw. - pub first_vertex: u32, + /// For `ArrayIndirectParameters`, `first_vertex`; for + /// `ElementIndirectParameters`, `first_index`. + pub first_vertex_or_first_index: u32, /// For `ArrayIndirectParameters`, `first_instance`; for /// `ElementIndirectParameters`, `base_vertex`. @@ -385,7 +386,7 @@ pub fn batch_and_prepare_sorted_render_phase( gpu_array_buffer: ResMut>, mut indirect_parameters_buffer: ResMut, mut sorted_render_phases: ResMut>, - mut views: Query<(Entity, Has)>, + mut views: Query<(Entity, Has), With>, system_param_item: StaticSystemParam, ) where I: CachedRenderPipelinePhaseItem + SortedPhaseItem, @@ -425,12 +426,17 @@ pub fn batch_and_prepare_sorted_render_phase( // Unpack that index and metadata. Note that it's possible for index // and/or metadata to not be present, which signifies that this // entity is unbatchable. In that case, we break the batch here. - let (mut current_input_index, mut current_meta) = (None, None); - if let Some((input_index, maybe_meta)) = current_batch_input_index { - current_input_index = Some(input_index); - current_meta = - maybe_meta.map(|meta| BatchMeta::new(&phase.items[current_index], meta)); - } + // If the index isn't present the item is not part of this pipeline and so will be skipped. + let Some((current_input_index, current_meta)) = current_batch_input_index else { + // Break a batch if we need to. + if let Some(batch) = batch.take() { + batch.flush(data_buffer.len() as u32, phase); + } + + continue; + }; + let current_meta = + current_meta.map(|meta| BatchMeta::new(&phase.items[current_index], meta)); // Determine if this entity can be included in the batch we're // building up. @@ -474,10 +480,9 @@ pub fn batch_and_prepare_sorted_render_phase( // Add a new preprocessing work item so that the preprocessing // shader will copy the per-instance data over. - if let (Some(batch), Some(input_index)) = (batch.as_ref(), current_input_index.as_ref()) - { + if let Some(batch) = batch.as_ref() { work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: (*input_index).into(), + input_index: current_input_index.into(), output_index: match batch.indirect_parameters_index { Some(indirect_parameters_index) => indirect_parameters_index.into(), None => output_index, @@ -498,7 +503,7 @@ pub fn batch_and_prepare_binned_render_phase( gpu_array_buffer: ResMut>, mut indirect_parameters_buffer: ResMut, mut binned_render_phases: ResMut>, - mut views: Query<(Entity, Has)>, + mut views: Query<(Entity, Has), With>, param: StaticSystemParam, ) where BPI: BinnedPhaseItem, diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index b950b2a6b93828..4f533345b94f7f 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -61,7 +61,7 @@ impl Default for Viewport { fn default() -> Self { Self { physical_position: Default::default(), - physical_size: Default::default(), + physical_size: UVec2::new(1, 1), depth: 0.0..1.0, } } @@ -211,7 +211,6 @@ pub struct Camera { #[reflect(ignore)] pub computed: ComputedCameraValues, /// The "target" that this camera will render to. - #[reflect(ignore)] pub target: RenderTarget, /// If this is set to `true`, the camera will use an intermediate "high dynamic range" render texture. /// This allows rendering with a wider range of lighting values. @@ -374,6 +373,39 @@ impl Camera { Some(viewport_position) } + /// Given a position in world space, use the camera to compute the viewport-space coordinates and depth. + /// + /// To get the coordinates in Normalized Device Coordinates, you should use + /// [`world_to_ndc`](Self::world_to_ndc). + /// + /// Returns `None` if any of these conditions occur: + /// - The computed coordinates are beyond the near or far plane + /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) + /// - The world coordinates cannot be mapped to the Normalized Device Coordinates. See [`world_to_ndc`](Camera::world_to_ndc) + /// May also panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc). + #[doc(alias = "world_to_screen_with_depth")] + pub fn world_to_viewport_with_depth( + &self, + camera_transform: &GlobalTransform, + world_position: Vec3, + ) -> Option { + let target_size = self.logical_viewport_size()?; + let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?; + // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space + if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 { + return None; + } + + // Stretching ndc depth to value via near plane and negating result to be in positive room again. + let depth = -self.depth_ndc_to_view_z(ndc_space_coords.z); + + // Once in NDC space, we can discard the z element and rescale x/y to fit the screen + let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size; + // Flip the Y co-ordinate origin from the bottom to the top. + viewport_position.y = target_size.y - viewport_position.y; + Some(viewport_position.extend(depth)) + } + /// Returns a ray originating from the camera, that passes through everything beyond `viewport_position`. /// /// The resulting ray starts on the near plane of the camera. @@ -479,6 +511,24 @@ impl Camera { (!world_space_coords.is_nan()).then_some(world_space_coords) } + + /// Converts the depth in Normalized Device Coordinates + /// to linear view z for perspective projections. + /// + /// Note: Depth values in front of the camera will be negative as -z is forward + pub fn depth_ndc_to_view_z(&self, ndc_depth: f32) -> f32 { + let near = self.clip_from_view().w_axis.z; // [3][2] + -near / ndc_depth + } + + /// Converts the depth in Normalized Device Coordinates + /// to linear view z for orthographic projections. + /// + /// Note: Depth values in front of the camera will be negative as -z is forward + pub fn depth_ndc_to_view_z_2d(&self, ndc_depth: f32) -> f32 { + -(self.clip_from_view().w_axis.z - ndc_depth) / self.clip_from_view().z_axis.z + // [3][2] [2][2] + } } /// Control how this camera outputs once rendering is completed. @@ -510,8 +560,8 @@ impl Default for CameraOutputMode { } /// Configures the [`RenderGraph`](crate::render_graph::RenderGraph) name assigned to be run for a given [`Camera`] entity. -#[derive(Component, Deref, DerefMut, Reflect, Clone)] -#[reflect_value(Component)] +#[derive(Component, Debug, Deref, DerefMut, Reflect, Clone)] +#[reflect_value(Component, Debug)] pub struct CameraRenderGraph(InternedRenderSubGraph); impl CameraRenderGraph { @@ -541,6 +591,12 @@ pub enum RenderTarget { TextureView(ManualTextureViewHandle), } +impl Default for RenderTarget { + fn default() -> Self { + Self::Window(Default::default()) + } +} + impl From> for RenderTarget { fn from(handle: Handle) -> Self { Self::Image(handle) @@ -561,12 +617,6 @@ pub enum NormalizedRenderTarget { TextureView(ManualTextureViewHandle), } -impl Default for RenderTarget { - fn default() -> Self { - Self::Window(Default::default()) - } -} - impl RenderTarget { /// Normalize the render target down to a more concrete value, mostly used for equality comparisons. pub fn normalize(&self, primary_window: Option) -> Option { diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 49f25530b13238..32dff45d1df7c0 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -1,7 +1,9 @@ // FIXME(3492): remove once docs are ready #![allow(missing_docs)] #![allow(unsafe_code)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +#![allow(internal_features)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" @@ -64,7 +66,7 @@ use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; -use crate::mesh::GpuMesh; +use crate::mesh::RenderMesh; use crate::renderer::WgpuWrapper; use crate::{ camera::CameraPlugin, @@ -102,7 +104,6 @@ pub struct RenderPlugin { /// The systems sets of the default [`App`] rendering schedule. /// -/// that runs immediately after the matching system set. /// These can be useful for ordering, but you almost never want to add your systems to these sets. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderSet { @@ -115,7 +116,7 @@ pub enum RenderSet { /// Queue drawable entities as phase items in render phases ready for /// sorting (if necessary) Queue, - /// A sub-set within [`Queue`](RenderSet::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::` is completed. + /// A sub-set within [`Queue`](RenderSet::Queue) where mesh entity queue systems are executed. Ensures `prepare_assets::` is completed. QueueMeshes, // TODO: This could probably be moved in favor of a system ordering // abstraction in `Render` or `Queue` @@ -136,6 +137,10 @@ pub enum RenderSet { Render, /// Cleanup render resources here. Cleanup, + /// Final cleanup occurs: all entities will be despawned. + /// + /// Runs after [`Cleanup`](RenderSet::Cleanup). + PostCleanup, } /// The main render schedule. @@ -160,12 +165,17 @@ impl Render { Prepare, Render, Cleanup, + PostCleanup, ) .chain(), ); schedule.configure_sets((ExtractCommands, PrepareAssets, Prepare).chain()); - schedule.configure_sets(QueueMeshes.in_set(Queue).after(prepare_assets::)); + schedule.configure_sets( + QueueMeshes + .in_set(Queue) + .after(prepare_assets::), + ); schedule.configure_sets( (PrepareResources, PrepareResourcesFlush, PrepareBindGroups) .chain() @@ -463,7 +473,7 @@ unsafe fn initialize_render_app(app: &mut App) { render_system, ) .in_set(RenderSet::Render), - World::clear_entities.in_set(RenderSet::Cleanup), + World::clear_entities.in_set(RenderSet::PostCleanup), ), ); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs new file mode 100644 index 00000000000000..218e19c475a7bb --- /dev/null +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -0,0 +1,1025 @@ +//! Manages mesh vertex and index buffers. + +use std::{ + borrow::Cow, + fmt::{self, Display, Formatter}, + iter, + ops::Range, + vec::Vec, +}; + +use bevy_app::{App, Plugin}; +use bevy_asset::AssetId; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + schedule::IntoSystemConfigs as _, + system::{Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_utils::{ + hashbrown::{HashMap, HashSet}, + tracing::error, +}; +use offset_allocator::{Allocation, Allocator}; +use wgpu::{ + util::BufferInitDescriptor, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, + DownlevelFlags, COPY_BUFFER_ALIGNMENT, +}; + +use crate::{ + mesh::{Indices, Mesh, MeshVertexBufferLayouts, RenderMesh}, + render_asset::{prepare_assets, ExtractedAssets}, + render_resource::Buffer, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, + Render, RenderApp, RenderSet, +}; + +/// A plugin that manages GPU memory for mesh data. +pub struct MeshAllocatorPlugin; + +/// Manages the assignment of mesh data to GPU buffers. +/// +/// The Bevy renderer tries to pack vertex and index data for multiple meshes +/// together so that multiple meshes can be drawn back-to-back without any +/// rebinding. This resource manages these buffers. +/// +/// Within each slab, or hardware buffer, the underlying allocation algorithm is +/// [`offset-allocator`], a Rust port of Sebastian Aaltonen's hard-real-time C++ +/// `OffsetAllocator`. Slabs start small and then grow as their contents fill +/// up, up to a maximum size limit. To reduce fragmentation, vertex and index +/// buffers that are too large bypass this system and receive their own buffers. +/// +/// The [`MeshAllocatorSettings`] allows you to tune the behavior of the +/// allocator for better performance with your application. Most applications +/// won't need to change the settings from their default values. +#[derive(Resource)] +pub struct MeshAllocator { + /// Holds all buffers and allocators. + slabs: HashMap, + + /// Maps a layout to the slabs that hold elements of that layout. + /// + /// This is used when allocating, so that we can find the appropriate slab + /// to place an object in. + slab_layouts: HashMap>, + + /// Maps mesh asset IDs to the ID of the slabs that hold their vertex data. + mesh_id_to_vertex_slab: HashMap, SlabId>, + + /// Maps mesh asset IDs to the ID of the slabs that hold their index data. + mesh_id_to_index_slab: HashMap, SlabId>, + + /// The next slab ID to assign. + next_slab_id: SlabId, + + /// Whether we can pack multiple vertex arrays into a single slab on this + /// platform. + /// + /// This corresponds to [`DownlevelFlags::BASE_VERTEX`], which is unset on + /// WebGL 2. On this platform, we must give each vertex array its own + /// buffer, because we can't adjust the first vertex when we perform a draw. + general_vertex_slabs_supported: bool, +} + +/// Tunable parameters that customize the behavior of the allocator. +/// +/// Generally, these parameters adjust the tradeoff between memory fragmentation +/// and performance. You can adjust them as desired for your application. Most +/// applications can stick with the default values. +#[derive(Resource)] +pub struct MeshAllocatorSettings { + /// The minimum size of a slab (hardware buffer), in bytes. + /// + /// The default value is 1 MiB. + pub min_slab_size: u64, + + /// The maximum size of a slab (hardware buffer), in bytes. + /// + /// When a slab reaches this limit, a new slab is created. + /// + /// The default value is 512 MiB. + pub max_slab_size: u64, + + /// The maximum size of vertex or index data that can be placed in a general + /// slab, in bytes. + /// + /// If a mesh has vertex or index data that exceeds this size limit, that + /// data is placed in its own slab. This reduces fragmentation, but incurs + /// more CPU-side binding overhead when drawing the mesh. + /// + /// The default value is 256 MiB. + pub large_threshold: u64, + + /// The factor by which we scale a slab when growing it. + /// + /// This value must be greater than 1. Higher values result in more + /// fragmentation but fewer expensive copy operations when growing the + /// buffer. + /// + /// The default value is 1.5. + pub growth_factor: f64, +} + +impl Default for MeshAllocatorSettings { + fn default() -> Self { + Self { + // 1 MiB + min_slab_size: 1024 * 1024, + // 512 MiB + max_slab_size: 1024 * 1024 * 512, + // 256 MiB + large_threshold: 1024 * 1024 * 256, + // 1.5× growth + growth_factor: 1.5, + } + } +} + +/// The hardware buffer that mesh data lives in, as well as the range within +/// that buffer. +pub struct MeshBufferSlice<'a> { + /// The buffer that the mesh data resides in. + pub buffer: &'a Buffer, + + /// The range of elements within this buffer that the mesh data resides in, + /// measured in elements. + /// + /// This is not a byte range; it's an element range. For vertex data, this + /// is measured in increments of a single vertex. (Thus, if a vertex is 32 + /// bytes long, then this range is in units of 32 bytes each.) For index + /// data, this is measured in increments of a single index value (2 or 4 + /// bytes). Draw commands generally take their ranges in elements, not + /// bytes, so this is the most convenient unit in this case. + pub range: Range, +} + +/// The index of a single slab. +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)] +#[repr(transparent)] +struct SlabId(u32); + +/// Data for a single slab. +#[allow(clippy::large_enum_variant)] +enum Slab { + /// A slab that can contain multiple objects. + General(GeneralSlab), + /// A slab that contains a single object. + LargeObject(LargeObjectSlab), +} + +/// A resizable slab that can contain multiple objects. +/// +/// This is the normal type of slab used for objects that are below the +/// [`MeshAllocatorSettings::large_threshold`]. Slabs are divided into *slots*, +/// which are described in detail in the [`ElementLayout`] documentation. +struct GeneralSlab { + /// The [`Allocator`] that manages the objects in this slab. + allocator: Allocator, + + /// The GPU buffer that backs this slab. + /// + /// This may be `None` if the buffer hasn't been created yet. We delay + /// creation of buffers until allocating all the meshes for a single frame, + /// so that we don't needlessly create and resize buffers when many meshes + /// load all at once. + buffer: Option, + + /// Allocations that are on the GPU. + /// + /// The range is in slots. + resident_allocations: HashMap, SlabAllocation>, + + /// Allocations that are waiting to be uploaded to the GPU. + /// + /// The range is in slots. + pending_allocations: HashMap, SlabAllocation>, + + /// The layout of a single element (vertex or index). + element_layout: ElementLayout, + + /// The size of this slab in slots. + slot_capacity: u32, +} + +/// A slab that contains a single object. +/// +/// Typically, this is for objects that exceed the +/// [`MeshAllocatorSettings::large_threshold`]. This is also for objects that +/// would ordinarily receive their own slab but can't because of platform +/// limitations, most notably vertex arrays on WebGL 2. +struct LargeObjectSlab { + /// The GPU buffer that backs this slab. + /// + /// This may be `None` if the buffer hasn't been created yet. + buffer: Option, + + /// The layout of a single element (vertex or index). + element_layout: ElementLayout, +} + +/// The type of element that a slab can store. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +enum ElementClass { + /// Data for a vertex. + Vertex, + /// A vertex index. + Index, +} + +/// Information about the size of individual elements (vertices or indices) +/// within a slab. +/// +/// Slab objects are allocated in units of *slots*. Usually, each element takes +/// up one slot, and so elements and slots are equivalent. Occasionally, +/// however, a slot may consist of 2 or even 4 elements. This occurs when the +/// size of an element isn't divisible by [`COPY_BUFFER_ALIGNMENT`]. When we +/// resize buffers, we perform GPU-to-GPU copies to shuffle the existing +/// elements into their new positions, and such copies must be on +/// [`COPY_BUFFER_ALIGNMENT`] boundaries. Slots solve this problem by +/// guaranteeing that the size of an allocation quantum is divisible by both the +/// size of an element and [`COPY_BUFFER_ALIGNMENT`], so we can relocate it +/// freely. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +struct ElementLayout { + /// Either a vertex or an index. + class: ElementClass, + + /// The size in bytes of a single element (vertex or index). + size: u64, + + /// The number of elements that make up a single slot. + /// + /// Usually, this is 1, but it can be different if [`ElementLayout::size`] + /// isn't divisible by 4. See the comment in [`ElementLayout`] for more + /// details. + elements_per_slot: u32, +} + +/// The location of an allocation and the slab it's contained in. +struct MeshAllocation { + /// The ID of the slab. + slab_id: SlabId, + /// Holds the actual allocation. + slab_allocation: SlabAllocation, +} + +/// An allocation within a slab. +#[derive(Clone)] +struct SlabAllocation { + /// The actual [`Allocator`] handle, needed to free the allocation. + allocation: Allocation, + /// The number of slots that this allocation takes up. + slot_count: u32, +} + +/// Holds information about all slabs scheduled to be allocated or reallocated. +#[derive(Default, Deref, DerefMut)] +struct SlabsToReallocate(HashMap); + +/// Holds information about a slab that's scheduled to be allocated or +/// reallocated. +#[derive(Default)] +struct SlabToReallocate { + /// Maps all allocations that need to be relocated to their positions within + /// the *new* slab. + allocations_to_copy: HashMap, SlabAllocation>, +} + +impl Display for SlabId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Plugin for MeshAllocatorPlugin { + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .init_resource::() + .add_systems( + Render, + allocate_and_free_meshes + .in_set(RenderSet::PrepareAssets) + .before(prepare_assets::), + ); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // The `RenderAdapter` isn't available until now, so we can't do this in + // [`Plugin::build`]. + render_app.init_resource::(); + } +} + +impl FromWorld for MeshAllocator { + fn from_world(world: &mut World) -> Self { + // Note whether we're on WebGL 2. In this case, we must give every + // vertex array its own slab. + let render_adapter = world.resource::(); + let general_vertex_slabs_supported = render_adapter + .get_downlevel_capabilities() + .flags + .contains(DownlevelFlags::BASE_VERTEX); + + Self { + slabs: HashMap::new(), + slab_layouts: HashMap::new(), + mesh_id_to_vertex_slab: HashMap::new(), + mesh_id_to_index_slab: HashMap::new(), + next_slab_id: SlabId(0), + general_vertex_slabs_supported, + } + } +} + +/// A system that processes newly-extracted or newly-removed meshes and writes +/// their data into buffers or frees their data as appropriate. +pub fn allocate_and_free_meshes( + mut mesh_allocator: ResMut, + mesh_allocator_settings: Res, + extracted_meshes: Res>, + mut mesh_vertex_buffer_layouts: ResMut, + render_device: Res, + render_queue: Res, +) { + // Process newly-added meshes. + mesh_allocator.allocate_meshes( + &mesh_allocator_settings, + &extracted_meshes, + &mut mesh_vertex_buffer_layouts, + &render_device, + &render_queue, + ); + + // Process removed meshes. + mesh_allocator.free_meshes(&extracted_meshes); +} + +impl MeshAllocator { + /// Returns the buffer and range within that buffer of the vertex data for + /// the mesh with the given ID. + /// + /// If the mesh wasn't allocated, returns None. + pub fn mesh_vertex_slice(&self, mesh_id: &AssetId) -> Option { + self.mesh_slice_in_slab(mesh_id, *self.mesh_id_to_vertex_slab.get(mesh_id)?) + } + + /// Returns the buffer and range within that buffer of the index data for + /// the mesh with the given ID. + /// + /// If the mesh has no index data or wasn't allocated, returns None. + pub fn mesh_index_slice(&self, mesh_id: &AssetId) -> Option { + self.mesh_slice_in_slab(mesh_id, *self.mesh_id_to_index_slab.get(mesh_id)?) + } + + /// Given a slab and a mesh with data located with it, returns the buffer + /// and range of that mesh data within the slab. + fn mesh_slice_in_slab( + &self, + mesh_id: &AssetId, + slab_id: SlabId, + ) -> Option { + match self.slabs.get(&slab_id)? { + Slab::General(ref general_slab) => { + let slab_allocation = general_slab.resident_allocations.get(mesh_id)?; + Some(MeshBufferSlice { + buffer: general_slab.buffer.as_ref()?, + range: (slab_allocation.allocation.offset + * general_slab.element_layout.elements_per_slot) + ..((slab_allocation.allocation.offset + slab_allocation.slot_count) + * general_slab.element_layout.elements_per_slot), + }) + } + + Slab::LargeObject(ref large_object_slab) => { + let buffer = large_object_slab.buffer.as_ref()?; + Some(MeshBufferSlice { + buffer, + range: 0..((buffer.size() / large_object_slab.element_layout.size) as u32), + }) + } + } + } + + /// Processes newly-loaded meshes, allocating room in the slabs for their + /// mesh data and performing upload operations as appropriate. + fn allocate_meshes( + &mut self, + mesh_allocator_settings: &MeshAllocatorSettings, + extracted_meshes: &ExtractedAssets, + mesh_vertex_buffer_layouts: &mut MeshVertexBufferLayouts, + render_device: &RenderDevice, + render_queue: &RenderQueue, + ) { + let mut slabs_to_grow = SlabsToReallocate::default(); + + // Allocate. + for (mesh_id, mesh) in &extracted_meshes.extracted { + // Allocate vertex data. Note that we can only pack mesh vertex data + // together if the platform supports it. + let vertex_element_layout = ElementLayout::vertex(mesh_vertex_buffer_layouts, mesh); + if self.general_vertex_slabs_supported { + self.allocate( + mesh_id, + mesh.get_vertex_buffer_data().len() as u64, + vertex_element_layout, + &mut slabs_to_grow, + mesh_allocator_settings, + ); + } else { + self.allocate_large(mesh_id, vertex_element_layout); + } + + // Allocate index data. + if let (Some(index_buffer_data), Some(index_element_layout)) = + (mesh.get_index_buffer_bytes(), ElementLayout::index(mesh)) + { + self.allocate( + mesh_id, + index_buffer_data.len() as u64, + index_element_layout, + &mut slabs_to_grow, + mesh_allocator_settings, + ); + } + } + + // Perform growth. + for (slab_id, slab_to_grow) in slabs_to_grow.0 { + self.reallocate_slab(render_device, render_queue, slab_id, slab_to_grow); + } + + // Copy new mesh data in. + for (mesh_id, mesh) in &extracted_meshes.extracted { + self.copy_mesh_vertex_data(mesh_id, mesh, render_device, render_queue); + self.copy_mesh_index_data(mesh_id, mesh, render_device, render_queue); + } + } + + /// Copies vertex array data from a mesh into the appropriate spot in the + /// slab. + fn copy_mesh_vertex_data( + &mut self, + mesh_id: &AssetId, + mesh: &Mesh, + render_device: &RenderDevice, + render_queue: &RenderQueue, + ) { + let Some(&slab_id) = self.mesh_id_to_vertex_slab.get(mesh_id) else { + return; + }; + let vertex_data = mesh.get_vertex_buffer_data(); + + // Call the generic function. + self.copy_element_data( + mesh_id, + mesh, + &vertex_data, + BufferUsages::VERTEX, + slab_id, + render_device, + render_queue, + ); + } + + /// Copies index array data from a mesh into the appropriate spot in the + /// slab. + fn copy_mesh_index_data( + &mut self, + mesh_id: &AssetId, + mesh: &Mesh, + render_device: &RenderDevice, + render_queue: &RenderQueue, + ) { + let Some(&slab_id) = self.mesh_id_to_index_slab.get(mesh_id) else { + return; + }; + let Some(index_data) = mesh.get_index_buffer_bytes() else { + return; + }; + + // Call the generic function. + self.copy_element_data( + mesh_id, + mesh, + index_data, + BufferUsages::INDEX, + slab_id, + render_device, + render_queue, + ); + } + + /// A generic function that copies either vertex or index data into a slab. + #[allow(clippy::too_many_arguments)] + fn copy_element_data( + &mut self, + mesh_id: &AssetId, + mesh: &Mesh, + data: &[u8], + buffer_usages: BufferUsages, + slab_id: SlabId, + render_device: &RenderDevice, + render_queue: &RenderQueue, + ) { + let Some(slab) = self.slabs.get_mut(&slab_id) else { + return; + }; + + match *slab { + Slab::General(ref mut general_slab) => { + let (Some(ref buffer), Some(allocated_range)) = ( + &general_slab.buffer, + general_slab.pending_allocations.remove(mesh_id), + ) else { + return; + }; + + let slot_size = general_slab.element_layout.slot_size(); + + // Write the data in. + render_queue.write_buffer( + buffer, + allocated_range.allocation.offset as u64 * slot_size, + &pad_to_alignment(data, slot_size as usize), + ); + + // Mark the allocation as resident. + general_slab + .resident_allocations + .insert(*mesh_id, allocated_range); + } + + Slab::LargeObject(ref mut large_object_slab) => { + debug_assert!(large_object_slab.buffer.is_none()); + + // Create the buffer and its data in one go. + large_object_slab.buffer = Some(render_device.create_buffer_with_data( + &BufferInitDescriptor { + label: Some(&format!( + "large mesh slab {} ({}buffer)", + slab_id, + buffer_usages_to_str(buffer_usages) + )), + contents: &mesh.get_vertex_buffer_data(), + usage: buffer_usages | BufferUsages::COPY_DST, + }, + )); + } + } + } + + fn free_meshes(&mut self, extracted_meshes: &ExtractedAssets) { + let mut empty_slabs = HashSet::new(); + for mesh_id in &extracted_meshes.removed { + if let Some(slab_id) = self.mesh_id_to_vertex_slab.remove(mesh_id) { + self.free_allocation_in_slab(mesh_id, slab_id, &mut empty_slabs); + } + if let Some(slab_id) = self.mesh_id_to_index_slab.remove(mesh_id) { + self.free_allocation_in_slab(mesh_id, slab_id, &mut empty_slabs); + } + } + + for empty_slab in empty_slabs { + self.slabs.remove(&empty_slab); + } + } + + /// Given a slab and the ID of a mesh containing data in it, marks the + /// allocation as free. + /// + /// If this results in the slab becoming empty, this function adds the slab + /// to the `empty_slabs` set. + fn free_allocation_in_slab( + &mut self, + mesh_id: &AssetId, + slab_id: SlabId, + empty_slabs: &mut HashSet, + ) { + let Some(slab) = self.slabs.get_mut(&slab_id) else { + return; + }; + + match *slab { + Slab::General(ref mut general_slab) => { + let Some(slab_allocation) = general_slab + .resident_allocations + .remove(mesh_id) + .or_else(|| general_slab.pending_allocations.remove(mesh_id)) + else { + return; + }; + + general_slab.allocator.free(slab_allocation.allocation); + + if general_slab.is_empty() { + empty_slabs.insert(slab_id); + } + } + Slab::LargeObject(_) => { + empty_slabs.insert(slab_id); + } + } + } + + /// Allocates space for mesh data with the given byte size and layout in the + /// appropriate slab, creating that slab if necessary. + fn allocate( + &mut self, + mesh_id: &AssetId, + data_byte_len: u64, + layout: ElementLayout, + slabs_to_grow: &mut SlabsToReallocate, + settings: &MeshAllocatorSettings, + ) { + let data_element_count = data_byte_len.div_ceil(layout.size) as u32; + let data_slot_count = data_element_count.div_ceil(layout.elements_per_slot); + + // If the mesh data is too large for a slab, give it a slab of its own. + if data_slot_count as u64 * layout.slot_size() + >= settings.large_threshold.min(settings.max_slab_size) + { + self.allocate_large(mesh_id, layout); + } else { + self.allocate_general(mesh_id, data_slot_count, layout, slabs_to_grow, settings); + } + } + + /// Allocates space for mesh data with the given slot size and layout in the + /// appropriate general slab. + fn allocate_general( + &mut self, + mesh_id: &AssetId, + data_slot_count: u32, + layout: ElementLayout, + slabs_to_grow: &mut SlabsToReallocate, + settings: &MeshAllocatorSettings, + ) { + let candidate_slabs = self.slab_layouts.entry(layout).or_default(); + + // Loop through the slabs that accept elements of the appropriate type + // and try to allocate the mesh inside them. We go with the first one + // that succeeds. + let mut mesh_allocation = None; + 'slab: for &slab_id in &*candidate_slabs { + loop { + let Some(Slab::General(ref mut slab)) = self.slabs.get_mut(&slab_id) else { + unreachable!("Slab not found") + }; + + if let Some(allocation) = slab.allocator.allocate(data_slot_count) { + mesh_allocation = Some(MeshAllocation { + slab_id, + slab_allocation: SlabAllocation { + allocation, + slot_count: data_slot_count, + }, + }); + break 'slab; + } + + // Try to grow the slab. If this fails, the slab is full; go on + // to the next slab. + match slab.try_grow(settings) { + Ok(new_mesh_allocation_records) => { + slabs_to_grow.insert(slab_id, new_mesh_allocation_records); + } + Err(()) => continue 'slab, + } + } + } + + // If we still have no allocation, make a new slab. + if mesh_allocation.is_none() { + let new_slab_id = self.next_slab_id; + self.next_slab_id.0 += 1; + + let new_slab = GeneralSlab::new( + new_slab_id, + &mut mesh_allocation, + settings, + layout, + data_slot_count, + ); + + self.slabs.insert(new_slab_id, Slab::General(new_slab)); + candidate_slabs.push(new_slab_id); + slabs_to_grow.insert(new_slab_id, SlabToReallocate::default()); + } + + let mesh_allocation = mesh_allocation.expect("Should have been able to allocate"); + + // Mark the allocation as pending. Don't copy it in just yet; further + // meshes loaded this frame may result in its final allocation location + // changing. + if let Some(Slab::General(ref mut general_slab)) = + self.slabs.get_mut(&mesh_allocation.slab_id) + { + general_slab + .pending_allocations + .insert(*mesh_id, mesh_allocation.slab_allocation); + }; + + self.record_allocation(mesh_id, mesh_allocation.slab_id, layout.class); + } + + /// Allocates an object into its own dedicated slab. + fn allocate_large(&mut self, mesh_id: &AssetId, layout: ElementLayout) { + let new_slab_id = self.next_slab_id; + self.next_slab_id.0 += 1; + + self.record_allocation(mesh_id, new_slab_id, layout.class); + + self.slabs.insert( + new_slab_id, + Slab::LargeObject(LargeObjectSlab { + buffer: None, + element_layout: layout, + }), + ); + } + + /// Reallocates a slab that needs to be resized, or allocates a new slab. + /// + /// This performs the actual growth operation that [`GeneralSlab::try_grow`] + /// scheduled. We do the growth in two phases so that, if a slab grows + /// multiple times in the same frame, only one new buffer is reallocated, + /// rather than reallocating the buffer multiple times. + fn reallocate_slab( + &mut self, + render_device: &RenderDevice, + render_queue: &RenderQueue, + slab_id: SlabId, + slab_to_grow: SlabToReallocate, + ) { + let Some(Slab::General(slab)) = self.slabs.get_mut(&slab_id) else { + error!("Couldn't find slab {:?} to grow", slab_id); + return; + }; + + let old_buffer = slab.buffer.take(); + + let mut buffer_usages = BufferUsages::COPY_SRC | BufferUsages::COPY_DST; + match slab.element_layout.class { + ElementClass::Vertex => buffer_usages |= BufferUsages::VERTEX, + ElementClass::Index => buffer_usages |= BufferUsages::INDEX, + }; + + // Create the buffer. + let new_buffer = render_device.create_buffer(&BufferDescriptor { + label: Some(&format!( + "general mesh slab {} ({}buffer)", + slab_id, + buffer_usages_to_str(buffer_usages) + )), + size: slab.slot_capacity as u64 * slab.element_layout.slot_size(), + usage: buffer_usages, + mapped_at_creation: false, + }); + + slab.buffer = Some(new_buffer.clone()); + + // In order to do buffer copies, we need a command encoder. + let mut encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("slab resize encoder"), + }); + + // If we have no objects to copy over, we're done. + let Some(old_buffer) = old_buffer else { + return; + }; + + for (mesh_id, src_slab_allocation) in &mut slab.resident_allocations { + let Some(dest_slab_allocation) = slab_to_grow.allocations_to_copy.get(mesh_id) else { + continue; + }; + + encoder.copy_buffer_to_buffer( + &old_buffer, + src_slab_allocation.allocation.offset as u64 * slab.element_layout.slot_size(), + &new_buffer, + dest_slab_allocation.allocation.offset as u64 * slab.element_layout.slot_size(), + dest_slab_allocation.slot_count as u64 * slab.element_layout.slot_size(), + ); + // Now that we've done the copy, we can update the allocation record. + *src_slab_allocation = dest_slab_allocation.clone(); + } + + let command_buffer = encoder.finish(); + render_queue.submit([command_buffer]); + } + + /// Records the location of the given newly-allocated mesh data in the + /// [`Self::mesh_id_to_vertex_slab`] or [`Self::mesh_id_to_index_slab`] + /// tables as appropriate. + fn record_allocation( + &mut self, + mesh_id: &AssetId, + slab_id: SlabId, + element_class: ElementClass, + ) { + match element_class { + ElementClass::Vertex => { + self.mesh_id_to_vertex_slab.insert(*mesh_id, slab_id); + } + ElementClass::Index => { + self.mesh_id_to_index_slab.insert(*mesh_id, slab_id); + } + } + } +} + +impl GeneralSlab { + /// Creates a new growable slab big enough to hold an single element of + /// `data_slot_count` size with the given `layout`. + fn new( + new_slab_id: SlabId, + mesh_allocation: &mut Option, + settings: &MeshAllocatorSettings, + layout: ElementLayout, + data_slot_count: u32, + ) -> GeneralSlab { + let slab_slot_capacity = (settings.min_slab_size.div_ceil(layout.slot_size()) as u32) + .max(offset_allocator::ext::min_allocator_size(data_slot_count)); + + let mut new_slab = GeneralSlab { + allocator: Allocator::new(slab_slot_capacity), + buffer: None, + resident_allocations: HashMap::new(), + pending_allocations: HashMap::new(), + element_layout: layout, + slot_capacity: slab_slot_capacity, + }; + + // This should never fail. + if let Some(allocation) = new_slab.allocator.allocate(data_slot_count) { + *mesh_allocation = Some(MeshAllocation { + slab_id: new_slab_id, + slab_allocation: SlabAllocation { + slot_count: data_slot_count, + allocation, + }, + }); + } + + new_slab + } + + /// Attempts to grow a slab that's just run out of space. + /// + /// Returns a structure the allocations that need to be relocated if the + /// growth succeeded. If the slab is full, returns `Err`. + fn try_grow(&mut self, settings: &MeshAllocatorSettings) -> Result { + // In extremely rare cases due to allocator fragmentation, it may happen + // that we fail to re-insert every object that was in the slab after + // growing it. Even though this will likely never happen, we use this + // loop to handle this unlikely event properly if it does. + 'grow: loop { + let new_slab_slot_capacity = ((self.slot_capacity as f64 * settings.growth_factor) + .ceil() as u32) + .min((settings.max_slab_size / self.element_layout.slot_size()) as u32); + if new_slab_slot_capacity == self.slot_capacity { + // The slab is full. + return Err(()); + } + + // Grow the slab. + self.allocator = Allocator::new(new_slab_slot_capacity); + self.slot_capacity = new_slab_slot_capacity; + + let mut slab_to_grow = SlabToReallocate::default(); + + // Place every resident allocation that was in the old slab in the + // new slab. + for (allocated_mesh_id, old_allocation_range) in &self.resident_allocations { + let allocation_size = old_allocation_range.slot_count; + match self.allocator.allocate(allocation_size) { + Some(allocation) => { + slab_to_grow.allocations_to_copy.insert( + *allocated_mesh_id, + SlabAllocation { + allocation, + slot_count: allocation_size, + }, + ); + } + None => { + // We failed to insert one of the allocations that we + // had before. + continue 'grow; + } + } + } + + // Move every allocation that was pending in the old slab to the new + // slab. + for slab_allocation in self.pending_allocations.values_mut() { + let allocation_size = slab_allocation.slot_count; + match self.allocator.allocate(allocation_size) { + Some(allocation) => slab_allocation.allocation = allocation, + None => { + // We failed to insert one of the allocations that we + // had before. + continue 'grow; + } + } + } + + return Ok(slab_to_grow); + } + } +} + +impl ElementLayout { + /// Creates an [`ElementLayout`] for mesh data of the given class (vertex or + /// index) with the given byte size. + fn new(class: ElementClass, size: u64) -> ElementLayout { + ElementLayout { + class, + size, + // Make sure that slot boundaries begin and end on + // `COPY_BUFFER_ALIGNMENT`-byte (4-byte) boundaries. + elements_per_slot: (COPY_BUFFER_ALIGNMENT / gcd(size, COPY_BUFFER_ALIGNMENT)) as u32, + } + } + + fn slot_size(&self) -> u64 { + self.size * self.elements_per_slot as u64 + } + + /// Creates the appropriate [`ElementLayout`] for the given mesh's vertex + /// data. + fn vertex( + mesh_vertex_buffer_layouts: &mut MeshVertexBufferLayouts, + mesh: &Mesh, + ) -> ElementLayout { + let mesh_vertex_buffer_layout = + mesh.get_mesh_vertex_buffer_layout(mesh_vertex_buffer_layouts); + ElementLayout::new( + ElementClass::Vertex, + mesh_vertex_buffer_layout.0.layout().array_stride, + ) + } + + /// Creates the appropriate [`ElementLayout`] for the given mesh's index + /// data. + fn index(mesh: &Mesh) -> Option { + let size = match mesh.indices()? { + Indices::U16(_) => 2, + Indices::U32(_) => 4, + }; + Some(ElementLayout::new(ElementClass::Index, size)) + } +} + +impl GeneralSlab { + /// Returns true if this slab is empty. + fn is_empty(&self) -> bool { + self.resident_allocations.is_empty() && self.pending_allocations.is_empty() + } +} + +/// Returns the greatest common divisor of the two numbers. +/// +/// +fn gcd(mut a: u64, mut b: u64) -> u64 { + while b != 0 { + let t = b; + b = a % b; + a = t; + } + a +} + +/// Ensures that the size of a buffer is a multiple of the given alignment by +/// padding it with zeroes if necessary. +/// +/// If the buffer already has the required size, then this function doesn't +/// allocate. Otherwise, it copies the buffer into a new one and writes the +/// appropriate number of zeroes to the end. +fn pad_to_alignment(buffer: &[u8], align: usize) -> Cow<[u8]> { + if buffer.len() % align == 0 { + return Cow::Borrowed(buffer); + } + let mut buffer = buffer.to_vec(); + buffer.extend(iter::repeat(0).take(align - buffer.len() % align)); + Cow::Owned(buffer) +} + +/// Returns a string describing the given buffer usages. +fn buffer_usages_to_str(buffer_usages: BufferUsages) -> &'static str { + if buffer_usages.contains(BufferUsages::VERTEX) { + "vertex " + } else if buffer_usages.contains(BufferUsages::INDEX) { + "index " + } else { + "" + } +} diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 02dd99e15a6887..954a585b57c99b 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -8,8 +8,7 @@ use crate::{ prelude::Image, primitives::Aabb, render_asset::{PrepareAssetError, RenderAsset, RenderAssetUsages, RenderAssets}, - render_resource::{Buffer, TextureView, VertexBufferLayout}, - renderer::RenderDevice, + render_resource::{TextureView, VertexBufferLayout}, texture::GpuImage, }; use bevy_asset::{Asset, Handle}; @@ -24,10 +23,7 @@ use bevy_utils::tracing::{error, warn}; use bytemuck::cast_slice; use std::{collections::BTreeMap, hash::Hash, iter::FusedIterator}; use thiserror::Error; -use wgpu::{ - util::BufferInitDescriptor, BufferUsages, IndexFormat, VertexAttribute, VertexFormat, - VertexStepMode, -}; +use wgpu::{IndexFormat, VertexAttribute, VertexFormat, VertexStepMode}; use super::{MeshVertexBufferLayoutRef, MeshVertexBufferLayouts}; @@ -318,17 +314,19 @@ impl Mesh { /// Returns an iterator that yields references to the data of each vertex attribute. pub fn attributes( &self, - ) -> impl Iterator { - self.attributes.iter().map(|(id, data)| (*id, &data.values)) + ) -> impl Iterator { + self.attributes + .values() + .map(|data| (&data.attribute, &data.values)) } /// Returns an iterator that yields mutable references to the data of each vertex attribute. pub fn attributes_mut( &mut self, - ) -> impl Iterator { + ) -> impl Iterator { self.attributes - .iter_mut() - .map(|(id, data)| (*id, &mut data.values)) + .values_mut() + .map(|data| (&data.attribute, &mut data.values)) } /// Sets the vertex indices of the mesh. They describe how triangles are constructed out of the @@ -541,7 +539,7 @@ impl Mesh { /// Consumes the mesh and returns a mesh with no shared vertices. /// /// This can dramatically increase the vertex count, so make sure this is what you want. - /// Does nothing if no [Indices] are set. + /// Does nothing if no [`Indices`] are set. /// /// (Alternatively, you can use [`Mesh::duplicate_vertices`] to mutate an existing mesh in-place) #[must_use] @@ -550,6 +548,62 @@ impl Mesh { self } + /// Inverts the winding of the indices such that all counter-clockwise triangles are now + /// clockwise and vice versa. + /// For lines, their start and end indices are flipped. + /// + /// Does nothing if no [`Indices`] are set. + /// If this operation succeeded, an [`Ok`] result is returned. + pub fn invert_winding(&mut self) -> Result<(), MeshWindingInvertError> { + fn invert( + indices: &mut [I], + topology: PrimitiveTopology, + ) -> Result<(), MeshWindingInvertError> { + match topology { + PrimitiveTopology::TriangleList => { + // Early return if the index count doesn't match + if indices.len() % 3 != 0 { + return Err(MeshWindingInvertError::AbruptIndicesEnd); + } + for chunk in indices.chunks_mut(3) { + // This currently can only be optimized away with unsafe, rework this when `feature(slice_as_chunks)` gets stable. + let [_, b, c] = chunk else { + return Err(MeshWindingInvertError::AbruptIndicesEnd); + }; + std::mem::swap(b, c); + } + Ok(()) + } + PrimitiveTopology::LineList => { + // Early return if the index count doesn't match + if indices.len() % 2 != 0 { + return Err(MeshWindingInvertError::AbruptIndicesEnd); + } + indices.reverse(); + Ok(()) + } + PrimitiveTopology::TriangleStrip | PrimitiveTopology::LineStrip => { + indices.reverse(); + Ok(()) + } + _ => Err(MeshWindingInvertError::WrongTopology), + } + } + match &mut self.indices { + Some(Indices::U16(vec)) => invert(vec, self.primitive_topology), + Some(Indices::U32(vec)) => invert(vec, self.primitive_topology), + None => Ok(()), + } + } + + /// Consumes the mesh and returns a mesh with inverted winding of the indices such + /// that all counter-clockwise triangles are now clockwise and vice versa. + /// + /// Does nothing if no [`Indices`] are set. + pub fn with_inverted_winding(mut self) -> Result { + self.invert_winding().map(|_| self) + } + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. /// If the mesh is indexed, this defaults to smooth normals. Otherwise, it defaults to flat /// normals. @@ -747,13 +801,13 @@ impl Mesh { // The indices of `other` should start after the last vertex of `self`. let index_offset = self .attribute(Mesh::ATTRIBUTE_POSITION) - .get_or_insert(&VertexAttributeValues::Float32x3(Vec::default())) + .get_or_insert(&Float32x3(Vec::default())) .len(); // Extend attributes of `self` with attributes of `other`. - for (id, values) in self.attributes_mut() { + for (attribute, values) in self.attributes_mut() { let enum_variant_name = values.enum_variant_name(); - if let Some(other_values) = other.attribute(id) { + if let Some(other_values) = other.attribute(attribute.id) { match (values, other_values) { (Float32(vec1), Float32(vec2)) => vec1.extend(vec2), (Sint32(vec1), Sint32(vec2)) => vec1.extend(vec2), @@ -1187,6 +1241,20 @@ where } } +/// An error that occurred while trying to invert the winding of a [`Mesh`]. +#[derive(Debug, Error)] +pub enum MeshWindingInvertError { + /// This error occurs when you try to invert the winding for a mesh with [`PrimitiveTopology::PointList`]. + #[error("Mesh winding invertation does not work for primitive topology `PointList`")] + WrongTopology, + + /// This error occurs when you try to invert the winding for a mesh with + /// * [`PrimitiveTopology::TriangleList`], but the indices are not in chunks of 3. + /// * [`PrimitiveTopology::LineList`], but the indices are not in chunks of 2. + #[error("Indices weren't in chunks according to topology")] + AbruptIndicesEnd, +} + /// An error that occurred while trying to extract a collection of triangles from a [`Mesh`]. #[derive(Debug, Error)] pub enum MeshTrianglesError { @@ -1214,14 +1282,14 @@ impl core::ops::Mul for Transform { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct MeshVertexAttribute { /// The friendly name of the vertex attribute pub name: &'static str, /// The _unique_ id of the vertex attribute. This will also determine sort ordering /// when generating vertex buffers. Built-in / standard attributes will use "close to zero" - /// indices. When in doubt, use a random / very large usize to avoid conflicts. + /// indices. When in doubt, use a random / very large u64 to avoid conflicts. pub id: MeshVertexAttributeId, /// The format of the vertex attribute. @@ -1229,7 +1297,7 @@ pub struct MeshVertexAttribute { } impl MeshVertexAttribute { - pub const fn new(name: &'static str, id: usize, format: VertexFormat) -> Self { + pub const fn new(name: &'static str, id: u64, format: VertexFormat) -> Self { Self { name, id: MeshVertexAttributeId(id), @@ -1243,7 +1311,7 @@ impl MeshVertexAttribute { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub struct MeshVertexAttributeId(usize); +pub struct MeshVertexAttributeId(u64); impl From for MeshVertexAttributeId { fn from(attribute: MeshVertexAttribute) -> Self { @@ -1660,42 +1728,51 @@ impl BaseMeshPipelineKey { } } -/// The GPU-representation of a [`Mesh`]. -/// Consists of a vertex data buffer and an optional index data buffer. +/// The render world representation of a [`Mesh`]. #[derive(Debug, Clone)] -pub struct GpuMesh { - /// Contains all attribute data for each vertex. - pub vertex_buffer: Buffer, +pub struct RenderMesh { + /// The number of vertices in the mesh. pub vertex_count: u32, + + /// Morph targets for the mesh, if present. pub morph_targets: Option, - pub buffer_info: GpuBufferInfo, + + /// Information about the mesh data buffers, including whether the mesh uses + /// indices or not. + pub buffer_info: RenderMeshBufferInfo, + + /// Precomputed pipeline key bits for this mesh. pub key_bits: BaseMeshPipelineKey, + + /// A reference to the vertex buffer layout. + /// + /// Combined with [`RenderMesh::buffer_info`], this specifies the complete + /// layout of the buffers associated with this mesh. pub layout: MeshVertexBufferLayoutRef, } -impl GpuMesh { +impl RenderMesh { + /// Returns the primitive topology of this mesh (triangles, triangle strips, + /// etc.) #[inline] pub fn primitive_topology(&self) -> PrimitiveTopology { self.key_bits.primitive_topology() } } -/// The index/vertex buffer info of a [`GpuMesh`]. +/// The index/vertex buffer info of a [`RenderMesh`]. #[derive(Debug, Clone)] -pub enum GpuBufferInfo { +pub enum RenderMeshBufferInfo { Indexed { - /// Contains all index data of a mesh. - buffer: Buffer, count: u32, index_format: IndexFormat, }, NonIndexed, } -impl RenderAsset for GpuMesh { +impl RenderAsset for RenderMesh { type SourceAsset = Mesh; type Param = ( - SRes, SRes>, SResMut, ); @@ -1717,12 +1794,10 @@ impl RenderAsset for GpuMesh { Some(vertex_size * vertex_count + index_bytes) } - /// Converts the extracted mesh a into [`GpuMesh`]. + /// Converts the extracted mesh into a [`RenderMesh`]. fn prepare_asset( mesh: Self::SourceAsset, - (render_device, images, ref mut mesh_vertex_buffer_layouts): &mut SystemParamItem< - Self::Param, - >, + (images, ref mut mesh_vertex_buffer_layouts): &mut SystemParamItem, ) -> Result> { let morph_targets = match mesh.morph_targets.as_ref() { Some(mt) => { @@ -1734,25 +1809,12 @@ impl RenderAsset for GpuMesh { None => None, }; - let vertex_buffer_data = mesh.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() { - 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(), - } - } else { - GpuBufferInfo::NonIndexed + let buffer_info = match mesh.indices() { + Some(indices) => RenderMeshBufferInfo::Indexed { + count: indices.len() as u32, + index_format: indices.into(), + }, + None => RenderMeshBufferInfo::NonIndexed, }; let mesh_vertex_buffer_layout = @@ -1764,8 +1826,7 @@ impl RenderAsset for GpuMesh { mesh.morph_targets.is_some(), ); - Ok(GpuMesh { - vertex_buffer, + Ok(RenderMesh { vertex_count: mesh.count_vertices() as u32, buffer_info, key_bits, @@ -1914,7 +1975,10 @@ fn scale_normal(normal: Vec3, scale_recip: Vec3) -> Vec3 { #[cfg(test)] mod tests { use super::Mesh; - use crate::{mesh::VertexAttributeValues, render_asset::RenderAssetUsages}; + use crate::{ + mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}, + render_asset::RenderAssetUsages, + }; use bevy_math::Vec3; use bevy_transform::components::Transform; use wgpu::PrimitiveTopology; @@ -1980,4 +2044,93 @@ mod tests { panic!("Mesh does not have a uv attribute"); } } + + #[test] + fn point_list_mesh_invert_winding() { + let mesh = Mesh::new(PrimitiveTopology::PointList, RenderAssetUsages::default()) + .with_inserted_indices(Indices::U32(vec![])); + assert!(matches!( + mesh.with_inverted_winding(), + Err(MeshWindingInvertError::WrongTopology) + )); + } + + #[test] + fn line_list_mesh_invert_winding() { + let mesh = Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(Indices::U32(vec![0, 1, 1, 2, 2, 3])); + let mesh = mesh.with_inverted_winding().unwrap(); + assert_eq!( + mesh.indices().unwrap().iter().collect::>(), + vec![3, 2, 2, 1, 1, 0] + ); + } + + #[test] + fn line_list_mesh_invert_winding_fail() { + let mesh = Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(Indices::U32(vec![0, 1, 1])); + assert!(matches!( + mesh.with_inverted_winding(), + Err(MeshWindingInvertError::AbruptIndicesEnd) + )); + } + + #[test] + fn line_strip_mesh_invert_winding() { + let mesh = Mesh::new(PrimitiveTopology::LineStrip, RenderAssetUsages::default()) + .with_inserted_indices(Indices::U32(vec![0, 1, 2, 3])); + let mesh = mesh.with_inverted_winding().unwrap(); + assert_eq!( + mesh.indices().unwrap().iter().collect::>(), + vec![3, 2, 1, 0] + ); + } + + #[test] + fn triangle_list_mesh_invert_winding() { + let mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(vec![ + 0, 3, 1, // First triangle + 1, 3, 2, // Second triangle + ])); + let mesh = mesh.with_inverted_winding().unwrap(); + assert_eq!( + mesh.indices().unwrap().iter().collect::>(), + vec![ + 0, 1, 3, // First triangle + 1, 2, 3, // Second triangle + ] + ); + } + + #[test] + fn triangle_list_mesh_invert_winding_fail() { + let mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(vec![0, 3, 1, 2])); + assert!(matches!( + mesh.with_inverted_winding(), + Err(MeshWindingInvertError::AbruptIndicesEnd) + )); + } + + #[test] + fn triangle_strip_mesh_invert_winding() { + let mesh = Mesh::new( + PrimitiveTopology::TriangleStrip, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(vec![0, 1, 2, 3])); + let mesh = mesh.with_inverted_winding().unwrap(); + assert_eq!( + mesh.indices().unwrap().iter().collect::>(), + vec![3, 2, 1, 0] + ); + } } diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index df336407164963..84accac6582361 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -1,8 +1,11 @@ #[allow(clippy::module_inception)] mod mesh; + +pub mod allocator; pub mod morph; pub mod primitives; +use allocator::MeshAllocatorPlugin; use bevy_utils::HashSet; pub use mesh::*; pub use primitives::*; @@ -27,7 +30,8 @@ impl Plugin for MeshPlugin { .register_type::() .register_type::>() // 'Mesh' must be prepared after 'Image' as meshes rely on the morph target image being ready - .add_plugins(RenderAssetPlugin::::default()); + .add_plugins(RenderAssetPlugin::::default()) + .add_plugins(MeshAllocatorPlugin); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; diff --git a/crates/bevy_render/src/mesh/morph.rs b/crates/bevy_render/src/mesh/morph.rs index b5eac7cdfec63e..8055d2975fa0ca 100644 --- a/crates/bevy_render/src/mesh/morph.rs +++ b/crates/bevy_render/src/mesh/morph.rs @@ -11,7 +11,7 @@ use bevy_hierarchy::Children; use bevy_math::Vec3; use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; -use std::{iter, mem}; +use std::{iter, mem::size_of}; use thiserror::Error; const MAX_TEXTURE_WIDTH: u32 = 2048; @@ -84,7 +84,7 @@ impl MorphTargetImage { }; let data = targets .flat_map(|mut attributes| { - let layer_byte_count = (padding + component_count) as usize * mem::size_of::(); + let layer_byte_count = (padding + component_count) as usize * size_of::(); let mut buffer = Vec::with_capacity(layer_byte_count); for _ in 0..vertex_count { let Some(to_add) = attributes.next() else { @@ -93,7 +93,7 @@ impl MorphTargetImage { buffer.extend_from_slice(bytemuck::bytes_of(&to_add)); } // Pad each layer so that they fit width * height - buffer.extend(iter::repeat(0).take(padding as usize * mem::size_of::())); + buffer.extend(iter::repeat(0).take(padding as usize * size_of::())); debug_assert_eq!(buffer.len(), layer_byte_count); buffer }) diff --git a/crates/bevy_render/src/mesh/primitives/dim2.rs b/crates/bevy_render/src/mesh/primitives/dim2.rs index 282e59b3518081..3bbd2be6d219e7 100644 --- a/crates/bevy_render/src/mesh/primitives/dim2.rs +++ b/crates/bevy_render/src/mesh/primitives/dim2.rs @@ -487,7 +487,7 @@ impl MeshBuilder for EllipseMeshBuilder { let mut uvs = Vec::with_capacity(resolution); // Add pi/2 so that there is a vertex at the top (sin is 1.0 and cos is 0.0) - let start_angle = std::f32::consts::FRAC_PI_2; + let start_angle = FRAC_PI_2; let step = std::f32::consts::TAU / self.resolution as f32; for i in 0..self.resolution { @@ -595,7 +595,7 @@ impl MeshBuilder for AnnulusMeshBuilder { // the vertices at `start_angle` are duplicated for the purposes of UV // mapping. Here, each iteration places a pair of vertices at a fixed // angle from the center of the annulus. - let start_angle = std::f32::consts::FRAC_PI_2; + let start_angle = FRAC_PI_2; let step = std::f32::consts::TAU / self.resolution as f32; for i in 0..=self.resolution { let theta = start_angle + i as f32 * step; diff --git a/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs b/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs index 7feff797a31d1d..883afe16b120d1 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/sphere.rs @@ -123,7 +123,7 @@ impl SphereMeshBuilder { let inclination = point.y.acos(); let azimuth = point.z.atan2(point.x); - let norm_inclination = inclination / std::f32::consts::PI; + let norm_inclination = inclination / PI; let norm_azimuth = 0.5 - (azimuth / std::f32::consts::TAU); [norm_azimuth, norm_inclination] diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 89478063dc25c4..921a2cbc443150 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -168,7 +168,7 @@ impl HalfSpace { /// Returns the unit normal vector of the bisecting plane that characterizes the `HalfSpace`. #[inline] pub fn normal(&self) -> Vec3A { - Vec3A::from(self.normal_d) + Vec3A::from_vec4(self.normal_d) } /// Returns the signed distance from the bisecting plane to the origin along diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index e3a6aab5fb637b..6ba181d77d6ba6 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -1,4 +1,6 @@ -use crate::{ExtractSchedule, MainWorld, Render, RenderApp, RenderSet}; +use crate::{ + render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, +}; use bevy_app::{App, Plugin, SubApp}; use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; use bevy_ecs::{ @@ -9,7 +11,10 @@ use bevy_ecs::{ }; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_render_macros::ExtractResource; -use bevy_utils::{tracing::debug, HashMap, HashSet}; +use bevy_utils::{ + tracing::{debug, error}, + HashMap, HashSet, +}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; use thiserror::Error; @@ -18,6 +23,8 @@ use thiserror::Error; pub enum PrepareAssetError { #[error("Failed to prepare asset")] RetryNextUpdate(E), + #[error("Failed to build bind group: {0}")] + AsBindGroupError(AsBindGroupError), } /// Describes how an asset gets extracted and prepared for rendering. @@ -114,7 +121,7 @@ impl Default for RenderAssetUsages { /// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until /// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another /// prepared [`RenderAsset`], for example `Mesh::prepare_asset` relies on `RenderAssets::` for morph -/// targets, so the plugin is created as `RenderAssetPlugin::::default()`. +/// targets, so the plugin is created as `RenderAssetPlugin::::default()`. pub struct RenderAssetPlugin { phantom: PhantomData (A, AFTER)>, } @@ -168,9 +175,16 @@ impl RenderAssetDependency for A { /// Temporarily stores the extracted and removed assets of the current frame. #[derive(Resource)] pub struct ExtractedAssets { - extracted: Vec<(AssetId, A::SourceAsset)>, - removed: HashSet>, - added: HashSet>, + /// The assets extracted this frame. + pub extracted: Vec<(AssetId, A::SourceAsset)>, + + /// IDs of the assets removed this frame. + /// + /// These assets will not be present in [`ExtractedAssets::extracted`]. + pub removed: HashSet>, + + /// IDs of the assets added this frame. + pub added: HashSet>, } impl Default for ExtractedAssets { @@ -238,7 +252,10 @@ impl FromWorld for CachedExtractRenderAssetSystemState { /// This system extracts all created or modified assets of the corresponding [`RenderAsset::SourceAsset`] type /// into the "render world". -fn extract_render_asset(mut commands: Commands, mut main_world: ResMut) { +pub(crate) fn extract_render_asset( + mut commands: Commands, + mut main_world: ResMut, +) { main_world.resource_scope( |world, mut cached_state: Mut>| { let (mut events, mut assets) = cached_state.state.get_mut(world); @@ -349,6 +366,12 @@ pub fn prepare_assets( Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { prepare_next_frame.assets.push((id, extracted_asset)); } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + std::any::type_name::() + ); + } } } @@ -381,6 +404,12 @@ pub fn prepare_assets( Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { prepare_next_frame.assets.push((id, extracted_asset)); } + Err(PrepareAssetError::AsBindGroupError(e)) => { + error!( + "{} Bind group construction failed: {e}", + std::any::type_name::() + ); + } } } diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 0558f7c0781642..8528457d420a16 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -3,6 +3,7 @@ use crate::{ Edge, InputSlotError, OutputSlotError, RenderGraphContext, RenderGraphError, RunSubGraphError, SlotInfo, SlotInfos, }, + render_phase::DrawError, renderer::RenderContext, }; pub use bevy_ecs::label::DynEq; @@ -97,6 +98,8 @@ pub enum NodeRunError { OutputSlotError(#[from] OutputSlotError), #[error("encountered an error when running a sub-graph")] RunSubGraphError(#[from] RunSubGraphError), + #[error("encountered an error when executing draw command")] + DrawError(#[from] DrawError), } /// A collection of input and output [`Edges`](Edge) for a [`Node`]. diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 980899b64bc449..0880ae797efd34 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -2,7 +2,7 @@ use crate::render_phase::{PhaseItem, TrackedRenderPass}; use bevy_app::{App, SubApp}; use bevy_ecs::{ entity::Entity, - query::{QueryState, ROQueryItem, ReadOnlyQueryData}, + query::{QueryEntityError, QueryState, ROQueryItem, ReadOnlyQueryData}, system::{ReadOnlySystemParam, Resource, SystemParam, SystemParamItem, SystemState}, world::World, }; @@ -13,6 +13,7 @@ use std::{ hash::Hash, sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; +use thiserror::Error; /// A draw function used to draw [`PhaseItem`]s. /// @@ -34,7 +35,17 @@ pub trait Draw: Send + Sync + 'static { pass: &mut TrackedRenderPass<'w>, view: Entity, item: &P, - ); + ) -> Result<(), DrawError>; +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum DrawError { + #[error("Failed to execute render command {0:?}")] + RenderCommandFailure(&'static str), + #[error("Failed to get execute view query")] + InvalidViewQuery, + #[error("View entity not found")] + ViewEntityNotFound, } // TODO: make this generic? @@ -212,11 +223,13 @@ pub trait RenderCommand { #[derive(Debug)] pub enum RenderCommandResult { Success, - Failure, + Skip, + Failure(&'static str), } macro_rules! render_command_tuple_impl { - ($(($name: ident, $view: ident, $entity: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $view: ident, $entity: ident)),*) => { + $(#[$meta])* impl),*> RenderCommand

for ($($name,)*) { type Param = ($($name::Param,)*); type ViewQuery = ($($name::ViewQuery,)*); @@ -232,14 +245,22 @@ macro_rules! render_command_tuple_impl { ) -> RenderCommandResult { match maybe_entities { None => { - $(if let RenderCommandResult::Failure = $name::render(_item, $view, None, $name, _pass) { - return RenderCommandResult::Failure; - })* + $( + match $name::render(_item, $view, None, $name, _pass) { + RenderCommandResult::Skip => return RenderCommandResult::Skip, + RenderCommandResult::Failure(reason) => return RenderCommandResult::Failure(reason), + _ => {}, + } + )* } Some(($($entity,)*)) => { - $(if let RenderCommandResult::Failure = $name::render(_item, $view, Some($entity), $name, _pass) { - return RenderCommandResult::Failure; - })* + $( + match $name::render(_item, $view, Some($entity), $name, _pass) { + RenderCommandResult::Skip => return RenderCommandResult::Skip, + RenderCommandResult::Failure(reason) => return RenderCommandResult::Failure(reason), + _ => {}, + } + )* } } RenderCommandResult::Success @@ -248,7 +269,15 @@ macro_rules! render_command_tuple_impl { }; } -all_tuples!(render_command_tuple_impl, 0, 15, C, V, E); +all_tuples!( + #[doc(fake_variadic)] + 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. /// @@ -290,12 +319,23 @@ where pass: &mut TrackedRenderPass<'w>, view: Entity, item: &P, - ) { + ) -> Result<(), DrawError> { let param = self.state.get_manual(world); - let view = self.view.get_manual(world, view).unwrap(); + let view = match self.view.get_manual(world, view) { + Ok(view) => view, + Err(err) => match err { + QueryEntityError::NoSuchEntity(_) => return Err(DrawError::ViewEntityNotFound), + QueryEntityError::QueryDoesNotMatch(_) | QueryEntityError::AliasedMutability(_) => { + return Err(DrawError::InvalidViewQuery) + } + }, + }; + let entity = self.entity.get_manual(world, item.entity()).ok(); - // TODO: handle/log `RenderCommand` failure - C::render(item, view, entity, param, pass); + match C::render(item, view, entity, param, pass) { + RenderCommandResult::Success | RenderCommandResult::Skip => Ok(()), + RenderCommandResult::Failure(reason) => Err(DrawError::RenderCommandFailure(reason)), + } } } diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index 41482de6a8e00e..d7758452f701c8 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -606,7 +606,7 @@ impl<'a> TrackedRenderPass<'a> { } impl WriteTimestamp for TrackedRenderPass<'_> { - fn write_timestamp(&mut self, query_set: &wgpu::QuerySet, index: u32) { + fn write_timestamp(&mut self, query_set: &QuerySet, index: u32) { self.pass.write_timestamp(query_set, index); } } diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 00e32d4ea3bd5d..1bc1ee52ca6875 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -314,7 +314,7 @@ where render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, - ) { + ) -> Result<(), DrawError> { { let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); @@ -323,9 +323,11 @@ where // locks. } - self.render_batchable_meshes(render_pass, world, view); - self.render_unbatchable_meshes(render_pass, world, view); - self.render_non_meshes(render_pass, world, view); + self.render_batchable_meshes(render_pass, world, view)?; + self.render_unbatchable_meshes(render_pass, world, view)?; + self.render_non_meshes(render_pass, world, view)?; + + Ok(()) } /// Renders all batchable meshes queued in this phase. @@ -334,7 +336,7 @@ where render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, - ) { + ) -> Result<(), DrawError> { let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); @@ -355,9 +357,11 @@ where continue; }; - draw_function.draw(world, render_pass, view, &binned_phase_item); + draw_function.draw(world, render_pass, view, &binned_phase_item)?; } } + + Ok(()) } /// Renders all unbatchable meshes queued in this phase. @@ -366,7 +370,7 @@ where render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, - ) { + ) -> Result<(), DrawError> { let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); @@ -412,9 +416,10 @@ where continue; }; - draw_function.draw(world, render_pass, view, &binned_phase_item); + draw_function.draw(world, render_pass, view, &binned_phase_item)?; } } + Ok(()) } /// Renders all objects of type [`BinnedRenderPhaseType::NonMesh`]. @@ -425,7 +430,7 @@ where render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, - ) { + ) -> Result<(), DrawError> { let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); @@ -439,8 +444,10 @@ where continue; }; - draw_function.draw(world, render_pass, view, &binned_phase_item); + draw_function.draw(world, render_pass, view, &binned_phase_item)?; } + + Ok(()) } pub fn is_empty(&self) -> bool { @@ -769,8 +776,8 @@ where render_pass: &mut TrackedRenderPass<'w>, world: &'w World, view: Entity, - ) { - self.render_range(render_pass, world, view, ..); + ) -> Result<(), DrawError> { + self.render_range(render_pass, world, view, ..) } /// Renders all [`PhaseItem`]s in the provided `range` (based on their index in `self.items`) using their corresponding draw functions. @@ -780,7 +787,7 @@ where world: &'w World, view: Entity, range: impl SliceIndex<[I], Output = [I]>, - ) { + ) -> Result<(), DrawError> { let items = self .items .get(range) @@ -798,10 +805,11 @@ where index += 1; } else { let draw_function = draw_functions.get_mut(item.draw_function()).unwrap(); - draw_function.draw(world, render_pass, view, item); + draw_function.draw(world, render_pass, view, item)?; index += batch_range.len(); } } + Ok(()) } } @@ -1081,7 +1089,7 @@ impl RenderCommand

for SetItemPipeline { pass.set_render_pipeline(pipeline); RenderCommandResult::Success } else { - RenderCommandResult::Failure + RenderCommandResult::Skip } } } diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 39704525b780fb..6371678e48b3dd 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -352,6 +352,8 @@ pub enum AsBindGroupError { /// The bind group could not be generated. Try again next frame. #[error("The bind group could not be generated")] RetryNextUpdate, + #[error("At binding index{0}, the provided image sampler `{1}` does not match the required sampler type(s) `{2}`.")] + InvalidSamplerType(u32, String, String), } /// A prepared bind group returned as a result of [`AsBindGroup::as_bind_group`]. diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index 49be061aa98480..c50cf0583c59b9 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -1,4 +1,4 @@ -use std::{iter, marker::PhantomData}; +use std::{iter, marker::PhantomData, mem::size_of}; use crate::{ render_resource::Buffer, @@ -53,7 +53,7 @@ impl RawBufferVec { values: Vec::new(), buffer: None, capacity: 0, - item_size: std::mem::size_of::(), + item_size: size_of::(), buffer_usage, label: None, changed: false, @@ -387,7 +387,7 @@ where len: 0, buffer: None, capacity: 0, - item_size: std::mem::size_of::(), + item_size: size_of::(), buffer_usage, label: None, label_changed: false, @@ -444,7 +444,7 @@ where let size = self.item_size * capacity; self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor { label: self.label.as_deref(), - size: size as wgpu::BufferAddress, + size: size as BufferAddress, usage: BufferUsages::COPY_DST | self.buffer_usage, mapped_at_creation: false, })); diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index 365acddc951284..4b914ef413556a 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -33,18 +33,19 @@ pub use uniform_buffer::*; // TODO: decide where re-exports should go pub use wgpu::{ util::{BufferInitDescriptor, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder}, - 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, PipelineCompilationOptions, PipelineLayout, - PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, - RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, + AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, 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, + PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, + PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, + RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, StencilOperation, StencilState, StorageTextureAccess, StoreOp, TextureAspect, diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 4992ee9ed609e1..0c25d358d535fd 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -32,8 +32,8 @@ use wgpu::{ use crate::render_resource::resource_macros::*; -render_resource_wrapper!(ErasedShaderModule, wgpu::ShaderModule); -render_resource_wrapper!(ErasedPipelineLayout, wgpu::PipelineLayout); +render_resource_wrapper!(ErasedShaderModule, ShaderModule); +render_resource_wrapper!(ErasedPipelineLayout, PipelineLayout); /// A descriptor for a [`Pipeline`]. /// @@ -173,7 +173,7 @@ impl ShaderDefVal { impl ShaderCache { fn new(render_device: &RenderDevice, render_adapter: &RenderAdapter) -> Self { - let (capabilities, subgroup_stages) = get_capabilities( + let capabilities = get_capabilities( render_device.features(), render_adapter.get_downlevel_capabilities().flags, ); @@ -183,7 +183,7 @@ impl ShaderCache { #[cfg(not(debug_assertions))] let composer = naga_oil::compose::Composer::non_validating(); - let composer = composer.with_capabilities(capabilities, subgroup_stages); + let composer = composer.with_capabilities(capabilities); Self { composer, @@ -316,7 +316,7 @@ impl ShaderCache { }, )?; - wgpu::ShaderSource::Naga(Cow::Owned(naga)) + ShaderSource::Naga(Cow::Owned(naga)) } }; @@ -464,7 +464,7 @@ pub struct PipelineCache { waiting_pipelines: HashSet, new_pipelines: Mutex>, /// If `true`, disables asynchronous pipeline compilation. - /// This has no effect on MacOS, wasm, or without the `multi_threaded` feature. + /// This has no effect on macOS, wasm, or without the `multi_threaded` feature. synchronous_pipeline_compilation: bool, } @@ -742,6 +742,7 @@ impl PipelineCache { let compilation_options = PipelineCompilationOptions { constants: &std::collections::HashMap::new(), zero_initialize_workgroup_memory: false, + vertex_pulling_transform: Default::default(), }; let descriptor = RawRenderPipelineDescriptor { @@ -767,6 +768,7 @@ impl PipelineCache { // TODO: Should this be the same as the vertex compilation options? compilation_options, }), + cache: None, }; Ok(Pipeline::RenderPipeline( @@ -822,7 +824,9 @@ impl PipelineCache { compilation_options: PipelineCompilationOptions { constants: &std::collections::HashMap::new(), zero_initialize_workgroup_memory: false, + vertex_pulling_transform: Default::default(), }, + cache: None, }; Ok(Pipeline::ComputePipeline( @@ -992,14 +996,9 @@ pub enum PipelineCacheError { // TODO: This needs to be kept up to date with the capabilities in the `create_validator` function in wgpu-core // https://github.com/gfx-rs/wgpu/blob/trunk/wgpu-core/src/device/mod.rs#L449 -// We use a modified version of the `create_validator` function because `naga_oil`'s composer stores the capabilities -// and subgroup shader stages instead of a `Validator`. -// We also can't use that function because `wgpu-core` isn't included in WebGPU builds. -/// Get the device capabilities and subgroup support for use in `naga_oil`. -fn get_capabilities( - features: Features, - downlevel: DownlevelFlags, -) -> (Capabilities, naga::valid::ShaderStages) { +// We can't use the `wgpu-core` function to detect the device's capabilities because `wgpu-core` isn't included in WebGPU builds. +/// Get the device's capabilities for use in `naga_oil`. +fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabilities { let mut capabilities = Capabilities::empty(); capabilities.set( Capabilities::PUSH_CONSTANT, @@ -1042,6 +1041,16 @@ fn get_capabilities( Capabilities::SHADER_INT64, features.contains(Features::SHADER_INT64), ); + capabilities.set( + Capabilities::SHADER_INT64_ATOMIC_MIN_MAX, + features.intersects( + Features::SHADER_INT64_ATOMIC_MIN_MAX | Features::SHADER_INT64_ATOMIC_ALL_OPS, + ), + ); + capabilities.set( + Capabilities::SHADER_INT64_ATOMIC_ALL_OPS, + features.contains(Features::SHADER_INT64_ATOMIC_ALL_OPS), + ); capabilities.set( Capabilities::MULTISAMPLED_SHADING, downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING), @@ -1062,16 +1071,10 @@ fn get_capabilities( Capabilities::SUBGROUP_BARRIER, features.intersects(Features::SUBGROUP_BARRIER), ); - - let mut subgroup_stages = naga::valid::ShaderStages::empty(); - subgroup_stages.set( - naga::valid::ShaderStages::COMPUTE | naga::valid::ShaderStages::FRAGMENT, - features.contains(Features::SUBGROUP), - ); - subgroup_stages.set( - naga::valid::ShaderStages::VERTEX, + capabilities.set( + Capabilities::SUBGROUP_VERTEX_STAGE, features.contains(Features::SUBGROUP_VERTEX), ); - (capabilities, subgroup_stages) + capabilities } diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 51469a56d0be48..478128f2198c36 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -210,7 +210,7 @@ pub async fn initialize_renderer( let mut limits = options.limits.clone(); if matches!(options.priority, WgpuSettingsPriority::Functionality) { features = adapter.features(); - if adapter_info.device_type == wgpu::DeviceType::DiscreteGpu { + if adapter_info.device_type == DeviceType::DiscreteGpu { // `MAPPABLE_PRIMARY_BUFFERS` can have a significant, negative performance impact for // discrete GPUs due to having to transfer data across the PCI-E bus and so it // should not be automatically enabled in this case. It is however beneficial for @@ -355,6 +355,7 @@ pub async fn initialize_renderer( label: options.device_label.as_ref().map(AsRef::as_ref), required_features: features, required_limits: limits, + memory_hints: options.memory_hints.clone(), }, trace_path, ) @@ -431,7 +432,7 @@ impl<'w> RenderContext<'w> { /// configured using the provided `descriptor`. pub fn begin_tracked_render_pass<'a>( &'a mut self, - descriptor: RenderPassDescriptor<'a, '_>, + descriptor: RenderPassDescriptor<'_>, ) -> TrackedRenderPass<'a> { // Cannot use command_encoder() as we need to split the borrow on self let command_encoder = self.command_encoder.get_or_insert_with(|| { diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index b54cf8b4d920bb..1f9df24a78de5c 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; pub use wgpu::{ Backends, Dx12Compiler, Features as WgpuFeatures, Gles3MinorVersion, InstanceFlags, - Limits as WgpuLimits, PowerPreference, + Limits as WgpuLimits, MemoryHints, PowerPreference, }; /// Configures the priority used when automatically configuring the features/limits of `wgpu`. @@ -50,6 +50,8 @@ pub struct WgpuSettings { pub gles3_minor_version: Gles3MinorVersion, /// These are for controlling WGPU's debug information to eg. enable validation and shader debug info in release builds. pub instance_flags: InstanceFlags, + /// This hints to the WGPU device about the preferred memory allocation strategy. + pub memory_hints: MemoryHints, } impl Default for WgpuSettings { @@ -113,6 +115,7 @@ impl Default for WgpuSettings { dx12_shader_compiler: dx12_compiler, gles3_minor_version, instance_flags, + memory_hints: MemoryHints::default(), } } } diff --git a/crates/bevy_render/src/spatial_bundle.rs b/crates/bevy_render/src/spatial_bundle.rs index d8e749cf0bfc2d..e84f765a27fe8b 100644 --- a/crates/bevy_render/src/spatial_bundle.rs +++ b/crates/bevy_render/src/spatial_bundle.rs @@ -41,7 +41,7 @@ impl SpatialBundle { } } - /// A visible [`SpatialBundle`], with no translation, rotation, and a scale of 1 on all axes. + /// A [`SpatialBundle`] with inherited visibility and identity transform. pub const INHERITED_IDENTITY: Self = SpatialBundle { visibility: Visibility::Inherited, inherited_visibility: InheritedVisibility::HIDDEN, @@ -50,7 +50,7 @@ impl SpatialBundle { global_transform: GlobalTransform::IDENTITY, }; - /// An invisible [`SpatialBundle`], with no translation, rotation, and a scale of 1 on all axes. + /// An invisible [`SpatialBundle`] with identity transform. pub const HIDDEN_IDENTITY: Self = SpatialBundle { visibility: Visibility::Hidden, ..Self::INHERITED_IDENTITY diff --git a/crates/bevy_render/src/texture/dds.rs b/crates/bevy_render/src/texture/dds.rs index cddcd9d8f29359..29454278d39783 100644 --- a/crates/bevy_render/src/texture/dds.rs +++ b/crates/bevy_render/src/texture/dds.rs @@ -8,6 +8,7 @@ use wgpu::{ use super::{CompressedImageFormats, Image, TextureError}; +#[cfg(feature = "dds")] pub fn dds_buffer_to_image( #[cfg(debug_assertions)] name: String, buffer: &[u8], @@ -82,6 +83,7 @@ pub fn dds_buffer_to_image( Ok(image) } +#[cfg(feature = "dds")] pub fn dds_format_to_texture_format( dds: &Dds, is_srgb: bool, diff --git a/crates/bevy_render/src/texture/exr_texture_loader.rs b/crates/bevy_render/src/texture/exr_texture_loader.rs index aba2d9196c52d6..8a8fd8c9463846 100644 --- a/crates/bevy_render/src/texture/exr_texture_loader.rs +++ b/crates/bevy_render/src/texture/exr_texture_loader.rs @@ -10,9 +10,11 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat}; /// Loads EXR textures as Texture assets #[derive(Clone, Default)] +#[cfg(feature = "exr")] pub struct ExrTextureLoader; #[derive(Serialize, Deserialize, Default, Debug)] +#[cfg(feature = "exr")] pub struct ExrTextureLoaderSettings { pub asset_usage: RenderAssetUsages, } @@ -20,6 +22,7 @@ pub struct ExrTextureLoaderSettings { /// Possible errors that can be produced by [`ExrTextureLoader`] #[non_exhaustive] #[derive(Debug, Error)] +#[cfg(feature = "exr")] pub enum ExrTextureLoaderError { #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index 3f7091349e09a6..f8d3c7a6176d7f 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -98,7 +98,7 @@ fn fallback_image_new( render_device.create_texture_with_data( render_queue, &image.texture_descriptor, - wgpu::util::TextureDataOrder::default(), + TextureDataOrder::default(), &image.data, ) } else { diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 28de7cd4869a77..4eca33b20fa62c 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -27,43 +27,66 @@ pub const SAMPLER_ASSET_INDEX: u64 = 1; #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum ImageFormat { Avif, + #[cfg(feature = "basis-universal")] Basis, + #[cfg(feature = "bmp")] Bmp, + #[cfg(feature = "dds")] Dds, Farbfeld, Gif, + #[cfg(feature = "exr")] OpenExr, + #[cfg(feature = "hdr")] Hdr, Ico, + #[cfg(feature = "jpeg")] Jpeg, + #[cfg(feature = "ktx2")] Ktx2, + #[cfg(feature = "png")] Png, + #[cfg(feature = "pnm")] Pnm, + #[cfg(feature = "tga")] Tga, Tiff, + #[cfg(feature = "webp")] WebP, } +macro_rules! feature_gate { + ($feature: tt, $value: ident) => {{ + #[cfg(not(feature = $feature))] + { + bevy_utils::tracing::warn!("feature \"{}\" is not enabled", $feature); + return None; + } + #[cfg(feature = $feature)] + ImageFormat::$value + }}; +} + impl ImageFormat { pub fn from_mime_type(mime_type: &str) -> Option { Some(match mime_type.to_ascii_lowercase().as_str() { "image/avif" => ImageFormat::Avif, - "image/bmp" | "image/x-bmp" => ImageFormat::Bmp, - "image/vnd-ms.dds" => ImageFormat::Dds, - "image/vnd.radiance" => ImageFormat::Hdr, + "image/bmp" | "image/x-bmp" => feature_gate!("bmp", Bmp), + "image/vnd-ms.dds" => feature_gate!("dds", Dds), + "image/vnd.radiance" => feature_gate!("hdr", Hdr), "image/gif" => ImageFormat::Gif, "image/x-icon" => ImageFormat::Ico, - "image/jpeg" => ImageFormat::Jpeg, - "image/ktx2" => ImageFormat::Ktx2, - "image/png" => ImageFormat::Png, - "image/x-exr" => ImageFormat::OpenExr, + "image/jpeg" => feature_gate!("jpeg", Jpeg), + "image/ktx2" => feature_gate!("ktx2", Ktx2), + "image/png" => feature_gate!("png", Png), + "image/x-exr" => feature_gate!("exr", OpenExr), "image/x-portable-bitmap" | "image/x-portable-graymap" | "image/x-portable-pixmap" - | "image/x-portable-anymap" => ImageFormat::Pnm, - "image/x-targa" | "image/x-tga" => ImageFormat::Tga, + | "image/x-portable-anymap" => feature_gate!("pnm", Pnm), + "image/x-targa" | "image/x-tga" => feature_gate!("tga", Tga), "image/tiff" => ImageFormat::Tiff, - "image/webp" => ImageFormat::WebP, + "image/webp" => feature_gate!("webp", WebP), _ => return None, }) } @@ -71,21 +94,21 @@ impl ImageFormat { pub fn from_extension(extension: &str) -> Option { Some(match extension.to_ascii_lowercase().as_str() { "avif" => ImageFormat::Avif, - "basis" => ImageFormat::Basis, - "bmp" => ImageFormat::Bmp, - "dds" => ImageFormat::Dds, + "basis" => feature_gate!("basis-universal", Basis), + "bmp" => feature_gate!("bmp", Bmp), + "dds" => feature_gate!("dds", Dds), "ff" | "farbfeld" => ImageFormat::Farbfeld, "gif" => ImageFormat::Gif, - "exr" => ImageFormat::OpenExr, - "hdr" => ImageFormat::Hdr, + "exr" => feature_gate!("exr", OpenExr), + "hdr" => feature_gate!("hdr", Hdr), "ico" => ImageFormat::Ico, - "jpg" | "jpeg" => ImageFormat::Jpeg, - "ktx2" => ImageFormat::Ktx2, - "pbm" | "pam" | "ppm" | "pgm" => ImageFormat::Pnm, - "png" => ImageFormat::Png, - "tga" => ImageFormat::Tga, + "jpg" | "jpeg" => feature_gate!("jpeg", Jpeg), + "ktx2" => feature_gate!("ktx2", Ktx2), + "pbm" | "pam" | "ppm" | "pgm" => feature_gate!("pnm", Pnm), + "png" => feature_gate!("png", Png), + "tga" => feature_gate!("tga", Tga), "tif" | "tiff" => ImageFormat::Tiff, - "webp" => ImageFormat::WebP, + "webp" => feature_gate!("webp", WebP), _ => return None, }) } @@ -93,39 +116,51 @@ impl ImageFormat { pub fn as_image_crate_format(&self) -> Option { Some(match self { ImageFormat::Avif => image::ImageFormat::Avif, + #[cfg(feature = "bmp")] ImageFormat::Bmp => image::ImageFormat::Bmp, + #[cfg(feature = "dds")] ImageFormat::Dds => image::ImageFormat::Dds, ImageFormat::Farbfeld => image::ImageFormat::Farbfeld, ImageFormat::Gif => image::ImageFormat::Gif, + #[cfg(feature = "exr")] ImageFormat::OpenExr => image::ImageFormat::OpenExr, + #[cfg(feature = "hdr")] ImageFormat::Hdr => image::ImageFormat::Hdr, ImageFormat::Ico => image::ImageFormat::Ico, + #[cfg(feature = "jpeg")] ImageFormat::Jpeg => image::ImageFormat::Jpeg, + #[cfg(feature = "png")] ImageFormat::Png => image::ImageFormat::Png, + #[cfg(feature = "pnm")] ImageFormat::Pnm => image::ImageFormat::Pnm, + #[cfg(feature = "tga")] ImageFormat::Tga => image::ImageFormat::Tga, ImageFormat::Tiff => image::ImageFormat::Tiff, + #[cfg(feature = "webp")] ImageFormat::WebP => image::ImageFormat::WebP, - ImageFormat::Basis | ImageFormat::Ktx2 => return None, + #[cfg(feature = "basis-universal")] + ImageFormat::Basis => return None, + #[cfg(feature = "ktx2")] + ImageFormat::Ktx2 => return None, }) } pub fn from_image_crate_format(format: image::ImageFormat) -> Option { Some(match format { image::ImageFormat::Avif => ImageFormat::Avif, - image::ImageFormat::Bmp => ImageFormat::Bmp, - image::ImageFormat::Dds => ImageFormat::Dds, + image::ImageFormat::Bmp => feature_gate!("bmp", Bmp), + image::ImageFormat::Dds => feature_gate!("dds", Dds), image::ImageFormat::Farbfeld => ImageFormat::Farbfeld, image::ImageFormat::Gif => ImageFormat::Gif, - image::ImageFormat::OpenExr => ImageFormat::OpenExr, - image::ImageFormat::Hdr => ImageFormat::Hdr, + image::ImageFormat::OpenExr => feature_gate!("exr", OpenExr), + image::ImageFormat::Hdr => feature_gate!("hdr", Hdr), image::ImageFormat::Ico => ImageFormat::Ico, - image::ImageFormat::Jpeg => ImageFormat::Jpeg, - image::ImageFormat::Png => ImageFormat::Png, - image::ImageFormat::Pnm => ImageFormat::Pnm, - image::ImageFormat::Tga => ImageFormat::Tga, + image::ImageFormat::Jpeg => feature_gate!("jpeg", Jpeg), + image::ImageFormat::Png => feature_gate!("png", Png), + image::ImageFormat::Pnm => feature_gate!("pnm", Pnm), + image::ImageFormat::Tga => feature_gate!("tga", Tga), image::ImageFormat::Tiff => ImageFormat::Tiff, - image::ImageFormat::WebP => ImageFormat::WebP, + image::ImageFormat::WebP => feature_gate!("webp", WebP), _ => return None, }) } @@ -744,7 +779,7 @@ impl Image { let image_crate_format = format .as_image_crate_format() .ok_or_else(|| TextureError::UnsupportedTextureFormat(format!("{format:?}")))?; - let mut reader = image::io::Reader::new(std::io::Cursor::new(buffer)); + let mut reader = image::ImageReader::new(std::io::Cursor::new(buffer)); reader.set_format(image_crate_format); reader.no_limits(); let dyn_img = reader.decode()?; diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_render/src/texture/ktx2.rs index 0c89dad60ea616..941cb4299aba4c 100644 --- a/crates/bevy_render/src/texture/ktx2.rs +++ b/crates/bevy_render/src/texture/ktx2.rs @@ -20,6 +20,7 @@ use wgpu::{ use super::{CompressedImageFormats, DataFormat, Image, TextureError, TranscodeFormat}; +#[cfg(feature = "ktx2")] pub fn ktx2_buffer_to_image( buffer: &[u8], supported_compressed_formats: CompressedImageFormats, @@ -387,6 +388,7 @@ pub fn get_transcoded_formats( } } +#[cfg(feature = "ktx2")] pub fn ktx2_get_texture_format>( ktx2: &ktx2::Reader, is_srgb: bool, @@ -473,6 +475,7 @@ fn sample_information_to_data_type( ) } +#[cfg(feature = "ktx2")] pub fn ktx2_dfd_to_texture_format( data_format_descriptor: &BasicDataFormatDescriptor, sample_information: &[SampleInformation], @@ -1194,6 +1197,7 @@ pub fn ktx2_dfd_to_texture_format( }) } +#[cfg(feature = "ktx2")] pub fn ktx2_format_to_texture_format( ktx2_format: ktx2::Format, is_srgb: bool, diff --git a/crates/bevy_render/src/texture/texture_cache.rs b/crates/bevy_render/src/texture/texture_cache.rs index 3c15f3354a83e6..81e1b1d81f5251 100644 --- a/crates/bevy_render/src/texture/texture_cache.rs +++ b/crates/bevy_render/src/texture/texture_cache.rs @@ -82,16 +82,22 @@ impl TextureCache { } } + /// Returns `true` if the texture cache contains no textures. + pub fn is_empty(&self) -> bool { + self.textures.is_empty() + } + /// Updates the cache and only retains recently used textures. pub fn update(&mut self) { - for textures in self.textures.values_mut() { + self.textures.retain(|_, textures| { for texture in textures.iter_mut() { texture.frames_since_last_use += 1; texture.taken = false; } textures.retain(|texture| texture.frames_since_last_use < 3); - } + !textures.is_empty() + }); } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 9c3caa9ebee94f..f9c4a5c59b4d74 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -5,12 +5,12 @@ use bevy_asset::{load_internal_asset, Handle}; pub use visibility::*; pub use window::*; +use crate::extract_component::ExtractComponentPlugin; use crate::{ camera::{ CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, ExtractedCamera, ManualTextureViews, MipBias, TemporalJitter, }, - extract_resource::{ExtractResource, ExtractResourcePlugin}, prelude::Shader, primitives::Frustum, render_asset::RenderAssets, @@ -28,6 +28,7 @@ use bevy_color::LinearRgba; use bevy_ecs::prelude::*; use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render_macros::ExtractComponent; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; use std::{ @@ -107,10 +108,9 @@ impl Plugin for ViewPlugin { .register_type::() .register_type::() .register_type::() - .init_resource::() // NOTE: windows.is_changed() handles cases where a window was resized .add_plugins(( - ExtractResourcePlugin::::default(), + ExtractComponentPlugin::::default(), VisibilityPlugin, VisibilityRangePlugin, )); @@ -139,24 +139,26 @@ impl Plugin for ViewPlugin { /// Configuration resource for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). /// -/// The number of samples to run for Multi-Sample Anti-Aliasing. Higher numbers result in -/// smoother edges. -/// Defaults to 4 samples. +/// The number of samples to run for Multi-Sample Anti-Aliasing for a given camera. Higher numbers +/// result in smoother edges. /// -/// Note that web currently only supports 1 or 4 samples. +/// Defaults to 4 samples. Some advanced rendering features may require that MSAA be disabled. /// -/// # Example -/// ``` -/// # use bevy_app::prelude::App; -/// # use bevy_render::prelude::Msaa; -/// App::new() -/// .insert_resource(Msaa::default()) -/// .run(); -/// ``` +/// Note that web currently only supports 1 or 4 samples. #[derive( - Resource, Default, Clone, Copy, ExtractResource, Reflect, PartialEq, PartialOrd, Eq, Hash, Debug, + Component, + Default, + Clone, + Copy, + ExtractComponent, + Reflect, + PartialEq, + PartialOrd, + Eq, + Hash, + Debug, )] -#[reflect(Resource, Default)] +#[reflect(Component, Default)] pub enum Msaa { Off = 1, Sample2 = 2, @@ -279,17 +281,17 @@ pub struct ColorGradingGlobal { /// The [`ColorGrading`] structure, packed into the most efficient form for the /// GPU. #[derive(Clone, Copy, Debug, ShaderType)] -struct ColorGradingUniform { - balance: Mat3, - saturation: Vec3, - contrast: Vec3, - gamma: Vec3, - gain: Vec3, - lift: Vec3, - midtone_range: Vec2, - exposure: f32, - hue: f32, - post_saturation: f32, +pub struct ColorGradingUniform { + pub balance: Mat3, + pub saturation: Vec3, + pub contrast: Vec3, + pub gamma: Vec3, + pub gain: Vec3, + pub lift: Vec3, + pub midtone_range: Vec2, + pub exposure: f32, + pub hue: f32, + pub post_saturation: f32, } /// A section of color grading values that can be selectively applied to @@ -406,20 +408,20 @@ impl ColorGrading { #[derive(Clone, ShaderType)] pub struct ViewUniform { - clip_from_world: Mat4, - unjittered_clip_from_world: Mat4, - world_from_clip: Mat4, - world_from_view: Mat4, - view_from_world: Mat4, - clip_from_view: Mat4, - view_from_clip: Mat4, - world_position: Vec3, - exposure: f32, + pub clip_from_world: Mat4, + pub unjittered_clip_from_world: Mat4, + pub world_from_clip: Mat4, + pub world_from_view: Mat4, + pub view_from_world: Mat4, + pub clip_from_view: Mat4, + pub view_from_clip: Mat4, + pub world_position: Vec3, + pub exposure: f32, // viewport(x_origin, y_origin, width, height) - viewport: Vec4, - frustum: [Vec4; 6], - color_grading: ColorGradingUniform, - mip_bias: f32, + pub viewport: Vec4, + pub frustum: [Vec4; 6], + pub color_grading: ColorGradingUniform, + pub mip_bias: f32, } #[derive(Resource)] @@ -797,7 +799,6 @@ pub fn prepare_view_targets( mut commands: Commands, windows: Res, images: Res>, - msaa: Res, clear_color_global: Res, render_device: Res, mut texture_cache: ResMut, @@ -806,12 +807,13 @@ pub fn prepare_view_targets( &ExtractedCamera, &ExtractedView, &CameraMainTextureUsages, + &Msaa, )>, manual_texture_views: Res, ) { let mut textures = HashMap::default(); let mut output_textures = HashMap::default(); - for (entity, camera, view, texture_usage) in cameras.iter() { + for (entity, camera, view, texture_usage, msaa) in cameras.iter() { let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) else { continue; @@ -847,7 +849,7 @@ pub fn prepare_view_targets( }; let (a, b, sampled, main_texture) = textures - .entry((camera.target.clone(), view.hdr)) + .entry((camera.target.clone(), view.hdr, msaa)) .or_insert_with(|| { let descriptor = TextureDescriptor { label: None, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 341404d189fdb6..345055ca3d3f2b 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -47,6 +47,39 @@ pub enum Visibility { Visible, } +impl Visibility { + /// Toggles between `Visibility::Inherited` and `Visibility::Visible`. + /// If the value is `Visibility::Hidden`, it remains unaffected. + #[inline] + pub fn toggle_inherited_visible(&mut self) { + *self = match *self { + Visibility::Inherited => Visibility::Visible, + Visibility::Visible => Visibility::Inherited, + _ => *self, + }; + } + /// Toggles between `Visibility::Inherited` and `Visibility::Hidden`. + /// If the value is `Visibility::Visible`, it remains unaffected. + #[inline] + pub fn toggle_inherited_hidden(&mut self) { + *self = match *self { + Visibility::Inherited => Visibility::Hidden, + Visibility::Hidden => Visibility::Inherited, + _ => *self, + }; + } + /// Toggles between `Visibility::Visible` and `Visibility::Hidden`. + /// If the value is `Visibility::Inherited`, it remains unaffected. + #[inline] + pub fn toggle_visible_hidden(&mut self) { + *self = match *self { + Visibility::Visible => Visibility::Hidden, + Visibility::Hidden => Visibility::Visible, + _ => *self, + }; + } +} + // Allows `&Visibility == Visibility` impl PartialEq for &Visibility { #[inline] @@ -496,12 +529,10 @@ pub fn check_visibility( #[cfg(test)] mod test { - use bevy_app::prelude::*; - use bevy_ecs::prelude::*; - use super::*; - + use bevy_app::prelude::*; use bevy_hierarchy::BuildChildren; + use std::mem::size_of; fn visibility_bundle(visibility: Visibility) -> VisibilityBundle { VisibilityBundle { @@ -763,8 +794,7 @@ mod test { #[test] fn ensure_visibility_enum_size() { - use std::mem; - assert_eq!(1, mem::size_of::()); - assert_eq!(1, mem::size_of::>()); + assert_eq!(1, size_of::()); + assert_eq!(1, size_of::>()); } } diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_render/src/view/visibility/render_layers.rs index a57eb0220f8a2e..63ba1fdf80e254 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_render/src/view/visibility/render_layers.rs @@ -58,6 +58,10 @@ impl Default for RenderLayers { impl RenderLayers { /// Create a new `RenderLayers` belonging to the given layer. + /// + /// This `const` constructor is limited to `size_of::()` layers. + /// If you need to support an arbitrary number of layers, use [`with`](RenderLayers::with) + /// or [`from_layers`](RenderLayers::from_layers). pub const fn layer(n: Layer) -> Self { let (buffer_index, bit) = Self::layer_info(n); assert!( @@ -135,7 +139,7 @@ impl RenderLayers { false } - /// get the bitmask representation of the contained layers + /// Get the bitmask representation of the contained layers. pub fn bits(&self) -> &[u64] { self.0.as_slice() } @@ -162,7 +166,7 @@ impl RenderLayers { return None; } let next = buffer.trailing_zeros() + 1; - buffer >>= next; + buffer = buffer.checked_shr(next).unwrap_or(0); layer += next as usize; Some(layer - 1) }) @@ -359,4 +363,10 @@ mod rendering_mask_tests { let layers = layers.without(77); assert!(layers.0.len() == 1); } + + #[test] + fn render_layer_iter_no_overflow() { + let layers = RenderLayers::from_layers(&[63]); + layers.iter().count(); + } } diff --git a/crates/bevy_render/src/view/window/cursor.rs b/crates/bevy_render/src/view/window/cursor.rs new file mode 100644 index 00000000000000..eed41cec114be7 --- /dev/null +++ b/crates/bevy_render/src/view/window/cursor.rs @@ -0,0 +1,175 @@ +use bevy_asset::{AssetId, Assets, Handle}; +use bevy_ecs::{ + change_detection::DetectChanges, + component::Component, + entity::Entity, + query::With, + reflect::ReflectComponent, + system::{Commands, Local, Query, Res}, + world::Ref, +}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_utils::{tracing::warn, HashSet}; +use bevy_window::{SystemCursorIcon, Window}; +use bevy_winit::{ + convert_system_cursor_icon, CursorSource, CustomCursorCache, CustomCursorCacheKey, + PendingCursor, +}; +use wgpu::TextureFormat; + +use crate::prelude::Image; + +/// Insert into a window entity to set the cursor for that window. +#[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] +#[reflect(Component, Debug, Default)] +pub enum CursorIcon { + /// Custom cursor image. + Custom(CustomCursor), + /// System provided cursor icon. + System(SystemCursorIcon), +} + +impl Default for CursorIcon { + fn default() -> Self { + CursorIcon::System(Default::default()) + } +} + +impl From for CursorIcon { + fn from(icon: SystemCursorIcon) -> Self { + CursorIcon::System(icon) + } +} + +impl From for CursorIcon { + fn from(cursor: CustomCursor) -> Self { + CursorIcon::Custom(cursor) + } +} + +/// Custom cursor image data. +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash)] +pub enum CustomCursor { + /// Image to use as a cursor. + Image { + /// The image must be in 8 bit int or 32 bit float rgba. PNG images + /// work well for this. + handle: Handle, + /// X and Y coordinates of the hotspot in pixels. The hotspot must be + /// within the image bounds. + hotspot: (u16, u16), + }, + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + /// A URL to an image to use as the cursor. + Url { + /// Web URL to an image to use as the cursor. PNGs preferred. Cursor + /// creation can fail if the image is invalid or not reachable. + url: String, + /// X and Y coordinates of the hotspot in pixels. The hotspot must be + /// within the image bounds. + hotspot: (u16, u16), + }, +} + +pub fn update_cursors( + mut commands: Commands, + mut windows: Query<(Entity, Ref), With>, + cursor_cache: Res, + images: Res>, + mut queue: Local>, +) { + for (entity, cursor) in windows.iter_mut() { + if !(queue.remove(&entity) || cursor.is_changed()) { + continue; + } + + let cursor_source = match cursor.as_ref() { + CursorIcon::Custom(CustomCursor::Image { handle, hotspot }) => { + let cache_key = match handle.id() { + AssetId::Index { index, .. } => { + CustomCursorCacheKey::AssetIndex(index.to_bits()) + } + AssetId::Uuid { uuid } => CustomCursorCacheKey::AssetUuid(uuid.as_u128()), + }; + + if cursor_cache.0.contains_key(&cache_key) { + CursorSource::CustomCached(cache_key) + } else { + let Some(image) = images.get(handle) else { + warn!( + "Cursor image {handle:?} is not loaded yet and couldn't be used. Trying again next frame." + ); + queue.insert(entity); + continue; + }; + let Some(rgba) = image_to_rgba_pixels(image) else { + warn!("Cursor image {handle:?} not accepted because it's not rgba8 or rgba32float format"); + continue; + }; + + let width = image.texture_descriptor.size.width; + let height = image.texture_descriptor.size.height; + let source = match bevy_winit::WinitCustomCursor::from_rgba( + rgba, + width as u16, + height as u16, + hotspot.0, + hotspot.1, + ) { + Ok(source) => source, + Err(err) => { + warn!("Cursor image {handle:?} is invalid: {err}"); + continue; + } + }; + + CursorSource::Custom((cache_key, source)) + } + } + #[cfg(all(target_family = "wasm", target_os = "unknown"))] + CursorIcon::Custom(CustomCursor::Url { url, hotspot }) => { + let cache_key = CustomCursorCacheKey::Url(url.clone()); + + if cursor_cache.0.contains_key(&cache_key) { + CursorSource::CustomCached(cache_key) + } else { + use bevy_winit::CustomCursorExtWebSys; + let source = + bevy_winit::WinitCustomCursor::from_url(url.clone(), hotspot.0, hotspot.1); + CursorSource::Custom((cache_key, source)) + } + } + CursorIcon::System(system_cursor_icon) => { + CursorSource::System(convert_system_cursor_icon(*system_cursor_icon)) + } + }; + + commands + .entity(entity) + .insert(PendingCursor(Some(cursor_source))); + } +} + +/// Returns the image data as a `Vec`. +/// Only supports rgba8 and rgba32float formats. +fn image_to_rgba_pixels(image: &Image) -> Option> { + match image.texture_descriptor.format { + TextureFormat::Rgba8Unorm + | TextureFormat::Rgba8UnormSrgb + | TextureFormat::Rgba8Snorm + | TextureFormat::Rgba8Uint + | TextureFormat::Rgba8Sint => Some(image.data.clone()), + TextureFormat::Rgba32Float => Some( + image + .data + .chunks(4) + .map(|chunk| { + let chunk = chunk.try_into().unwrap(); + let num = bytemuck::cast_ref::<[u8; 4], f32>(chunk); + (num * 255.0) as u8 + }) + .collect(), + ), + _ => None, + } +} diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index cf347586e6429f..027a15e467d9b7 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -6,7 +6,7 @@ use crate::{ texture::TextureFormatPixelInfo, Extract, ExtractSchedule, Render, RenderApp, RenderSet, WgpuWrapper, }; -use bevy_app::{App, Plugin}; +use bevy_app::{App, Last, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; #[cfg(target_os = "linux")] use bevy_utils::warn_once; @@ -14,6 +14,7 @@ use bevy_utils::{default, tracing::debug, HashSet}; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; +use bevy_winit::CustomCursorCache; use std::{ num::NonZeroU32, ops::{Deref, DerefMut}, @@ -24,19 +25,22 @@ use wgpu::{ TextureViewDescriptor, }; +pub mod cursor; pub mod screenshot; use screenshot::{ ScreenshotManager, ScreenshotPlugin, ScreenshotPreparedState, ScreenshotToScreenPipeline, }; -use super::Msaa; +use self::cursor::update_cursors; pub struct WindowRenderPlugin; impl Plugin for WindowRenderPlugin { fn build(&self, app: &mut App) { - app.add_plugins(ScreenshotPlugin); + app.add_plugins(ScreenshotPlugin) + .init_resource::() + .add_systems(Last, update_cursors); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -250,11 +254,9 @@ pub fn prepare_windows( mut windows: ResMut, mut window_surfaces: ResMut, render_device: Res, - render_adapter: Res, screenshot_pipeline: Res, pipeline_cache: Res, mut pipelines: ResMut>, - mut msaa: ResMut, #[cfg(target_os = "linux")] render_instance: Res, ) { for window in windows.windows.values_mut() { @@ -263,35 +265,6 @@ pub fn prepare_windows( continue; }; - // This is an ugly hack to work around drivers that don't support MSAA. - // This should be removed once https://github.com/bevyengine/bevy/issues/7194 lands and we're doing proper - // feature detection for MSAA. - // When removed, we can also remove the `.after(prepare_windows)` of `prepare_core_3d_depth_textures` and `prepare_prepass_textures` - let sample_flags = render_adapter - .get_texture_format_features(surface_data.configuration.format) - .flags; - - if !sample_flags.sample_count_supported(msaa.samples()) { - let fallback = if sample_flags.sample_count_supported(Msaa::default().samples()) { - Msaa::default() - } else { - Msaa::Off - }; - - let fallback_str = if fallback == Msaa::Off { - "disabling MSAA".to_owned() - } else { - format!("MSAA {}x", fallback.samples()) - }; - - bevy_utils::tracing::warn!( - "MSAA {}x is not supported on this device. Falling back to {}.", - msaa.samples(), - fallback_str, - ); - *msaa = fallback; - } - // A recurring issue is hitting `wgpu::SurfaceError::Timeout` on certain Linux // mesa driver implementations. This seems to be a quirk of some drivers. // We'd rather keep panicking when not on Linux mesa, because in those case, @@ -484,7 +457,7 @@ pub fn create_surfaces( } } - let configuration = wgpu::SurfaceConfiguration { + let configuration = SurfaceConfiguration { format, width: window.physical_width, height: window.physical_height, diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index b688acfcedca9b..8bdc32c2152328 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -40,5 +40,5 @@ rmp-serde = "1.1" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index 992d7ef6259c70..6667fe836d735e 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities}, world::World, }; -use bevy_reflect::{Reflect, TypePath, TypeRegistry}; +use bevy_reflect::{PartialReflect, TypePath, TypeRegistry}; use bevy_utils::TypeIdMap; #[cfg(feature = "serialize")] @@ -28,7 +28,7 @@ use serde::Serialize; #[derive(Asset, TypePath, Default)] pub struct DynamicScene { /// Resources stored in the dynamic scene. - pub resources: Vec>, + pub resources: Vec>, /// Entities contained in the dynamic scene. pub entities: Vec, } @@ -40,8 +40,8 @@ pub struct DynamicEntity { /// Components that reference this entity must consistently use this identifier. pub entity: Entity, /// A vector of boxed components that belong to the given entity and - /// implement the [`Reflect`] trait. - pub components: Vec>, + /// implement the [`PartialReflect`] trait. + pub components: Vec>, } impl DynamicScene { @@ -117,7 +117,11 @@ 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, &type_registry); + reflect_component.apply_or_insert( + entity_mut, + component.as_partial_reflect(), + &type_registry, + ); } } diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index ea14930466cf13..26084c1a3e27fd 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -6,7 +6,7 @@ use bevy_ecs::{ reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, world::World, }; -use bevy_reflect::Reflect; +use bevy_reflect::PartialReflect; use bevy_utils::default; use std::collections::BTreeMap; @@ -52,8 +52,10 @@ use std::collections::BTreeMap; /// # let entity = world.spawn(ComponentA).id(); /// let dynamic_scene = DynamicSceneBuilder::from_world(&world).extract_entity(entity).build(); /// ``` +/// +/// [`Reflect`]: bevy_reflect::Reflect pub struct DynamicSceneBuilder<'w> { - extracted_resources: BTreeMap>, + extracted_resources: BTreeMap>, extracted_scene: BTreeMap, component_filter: SceneFilter, resource_filter: SceneFilter, diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index fa00fde5cc5109..b7bc473ca06d48 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -56,7 +56,6 @@ impl Plugin for ScenePlugin { app.init_asset::() .init_asset::() .init_asset_loader::() - .add_event::() .init_resource::() .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 542a22c5dcb2f9..f852d356b390b5 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -1,6 +1,6 @@ -use crate::{DynamicScene, InstanceInfo, SceneSpawnError}; +use crate::{DynamicScene, SceneSpawnError}; use bevy_asset::Asset; -use bevy_ecs::entity::EntityHashMap; +use bevy_ecs::entity::{Entity, EntityHashMap}; use bevy_ecs::{ reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectResource}, world::World, @@ -43,7 +43,8 @@ impl Scene { /// provided [`AppTypeRegistry`] or doesn't reflect the [`Component`](bevy_ecs::component::Component) trait. pub fn clone_with(&self, type_registry: &AppTypeRegistry) -> Result { let mut new_world = World::new(); - self.write_to_world_with(&mut new_world, type_registry)?; + let mut entity_map = EntityHashMap::default(); + self.write_to_world_with(&mut new_world, &mut entity_map, type_registry)?; Ok(Self { world: new_world }) } @@ -54,12 +55,9 @@ impl Scene { pub fn write_to_world_with( &self, world: &mut World, + entity_map: &mut EntityHashMap, type_registry: &AppTypeRegistry, - ) -> Result { - let mut instance_info = InstanceInfo { - entity_map: EntityHashMap::default(), - }; - + ) -> Result<(), SceneSpawnError> { let type_registry = type_registry.read(); // Resources archetype @@ -94,8 +92,7 @@ impl Scene { for archetype in self.world.archetypes().iter() { for scene_entity in archetype.entities() { - let entity = *instance_info - .entity_map + let entity = entity_map .entry(scene_entity.id()) .or_insert_with(|| world.spawn_empty().id()); for component_id in archetype.components() { @@ -121,7 +118,7 @@ impl Scene { &self.world, world, scene_entity.id(), - entity, + *entity, &type_registry, ); } @@ -130,10 +127,10 @@ impl Scene { for registration in type_registry.iter() { if let Some(map_entities_reflect) = registration.data::() { - map_entities_reflect.map_all_entities(world, &mut instance_info.entity_map); + map_entities_reflect.map_all_entities(world, entity_map); } } - Ok(instance_info) + Ok(()) } } diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 45791eb1d61a75..9aa59bb6fd8ed9 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -13,15 +13,15 @@ use bevy_utils::{tracing::error, HashMap, HashSet}; use thiserror::Error; use uuid::Uuid; -/// Emitted when [`crate::SceneInstance`] becomes ready to use. +/// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. /// -/// See also [`SceneSpawner::instance_is_ready`]. +/// See also [`Trigger`], [`SceneSpawner::instance_is_ready`]. +/// +/// [`Trigger`]: bevy_ecs::observer::Trigger #[derive(Clone, Copy, Debug, Eq, PartialEq, Event)] pub struct SceneInstanceReady { - /// ID of the spawned instance. - pub id: InstanceId, - /// Entity to which the scene was spawned as a child. - pub parent: Option, + /// Instance which has been spawned. + pub instance_id: InstanceId, } /// Information about a scene instance. @@ -226,6 +226,7 @@ impl SceneSpawner { let scene = scenes .get(id) .ok_or(SceneSpawnError::NonExistentScene { id })?; + scene.write_to_world(world, entity_map) }) } @@ -234,27 +235,32 @@ impl SceneSpawner { pub fn spawn_sync( &mut self, world: &mut World, - id: AssetId, + id: impl Into>, ) -> Result { - self.spawn_sync_internal(world, id, InstanceId::new()) + let mut entity_map = EntityHashMap::default(); + let id = id.into(); + Self::spawn_sync_internal(world, id, &mut entity_map)?; + let instance_id = InstanceId::new(); + self.spawned_instances + .insert(instance_id, InstanceInfo { entity_map }); + Ok(instance_id) } fn spawn_sync_internal( - &mut self, world: &mut World, id: AssetId, - instance_id: InstanceId, - ) -> Result { + entity_map: &mut EntityHashMap, + ) -> Result<(), SceneSpawnError> { world.resource_scope(|world, scenes: Mut>| { let scene = scenes .get(id) .ok_or(SceneSpawnError::NonExistentRealScene { id })?; - let instance_info = - scene.write_to_world_with(world, &world.resource::().clone())?; - - self.spawned_instances.insert(instance_id, instance_info); - Ok(instance_id) + scene.write_to_world_with( + world, + entity_map, + &world.resource::().clone(), + ) }) } @@ -317,10 +323,8 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - world.send_event(SceneInstanceReady { - id: instance_id, - parent: None, - }); + // Defer via commands otherwise SceneSpawner is not available in the observer. + world.commands().trigger(SceneInstanceReady { instance_id }); } } Err(SceneSpawnError::NonExistentScene { .. }) => { @@ -334,15 +338,18 @@ impl SceneSpawner { let scenes_to_spawn = std::mem::take(&mut self.scenes_to_spawn); for (scene_handle, instance_id, parent) in scenes_to_spawn { - match self.spawn_sync_internal(world, scene_handle.id(), instance_id) { + let mut entity_map = EntityHashMap::default(); + + match Self::spawn_sync_internal(world, scene_handle.id(), &mut entity_map) { Ok(_) => { + self.spawned_instances + .insert(instance_id, InstanceInfo { entity_map }); + // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - world.send_event(SceneInstanceReady { - id: instance_id, - parent: None, - }); + // Defer via commands otherwise SceneSpawner is not available in the observer. + world.commands().trigger(SceneInstanceReady { instance_id }); } } Err(SceneSpawnError::NonExistentRealScene { .. }) => { @@ -381,10 +388,10 @@ impl SceneSpawner { } } - world.send_event(SceneInstanceReady { - id: instance_id, - parent: Some(parent), - }); + // Defer via commands otherwise SceneSpawner is not available in the observer. + world + .commands() + .trigger_targets(SceneInstanceReady { instance_id }, parent); } else { self.scenes_with_parent.push((instance_id, parent)); } @@ -468,7 +475,7 @@ mod tests { use bevy_app::App; use bevy_asset::Handle; use bevy_asset::{AssetPlugin, AssetServer}; - use bevy_ecs::event::EventReader; + use bevy_ecs::observer::Trigger; use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::query::With; use bevy_ecs::system::{Commands, Res, ResMut, RunSystemOnce}; @@ -534,9 +541,13 @@ mod tests { #[reflect(Component)] struct ComponentA; + #[derive(Resource, Default)] + struct TriggerCount(u32); + fn setup() -> App { let mut app = App::new(); app.add_plugins((AssetPlugin::default(), ScenePlugin)); + app.init_resource::(); app.register_type::(); app.world_mut().spawn(ComponentA); @@ -558,84 +569,68 @@ mod tests { ) } - #[test] - fn event_scene() { - let mut app = setup(); - - // Build scene. - let scene = build_scene(&mut app); - - // Spawn scene. - let scene_id = - app.world_mut() - .run_system_once(move |mut scene_spawner: ResMut<'_, SceneSpawner>| { - scene_spawner.spawn(scene.clone()) - }); - - // Check for event arrival. - app.update(); - app.world_mut().run_system_once( - move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| { - let mut events = ev_scene.read(); + fn build_dynamic_scene(app: &mut App) -> Handle { + app.world_mut() + .run_system_once(|world: &World, asset_server: Res<'_, AssetServer>| { + asset_server.add(DynamicScene::from_world(world)) + }) + } - let event = events.next().expect("found no `SceneInstanceReady` event"); + fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) { + // Add observer + app.world_mut().observe( + move |trigger: Trigger, + scene_spawner: Res, + mut trigger_count: ResMut| { assert_eq!( - event.id, scene_id, + trigger.event().instance_id, + scene_id, "`SceneInstanceReady` contains the wrong `InstanceId`" ); - - assert!(events.next().is_none(), "found more than one event"); + assert_eq!( + trigger.entity(), + scene_entity, + "`SceneInstanceReady` triggered on the wrong parent entity" + ); + assert!( + scene_spawner.instance_is_ready(trigger.event().instance_id), + "`InstanceId` is not ready" + ); + trigger_count.0 += 1; }, ); + + // Check observer is triggered once. + app.update(); + app.world_mut() + .run_system_once(|trigger_count: Res| { + assert_eq!( + trigger_count.0, 1, + "wrong number of `SceneInstanceReady` triggers" + ); + }); } #[test] - fn event_scene_as_child() { + fn observe_scene() { let mut app = setup(); // Build scene. let scene = build_scene(&mut app); - // Spawn scene as child. - let (scene_id, scene_entity) = app.world_mut().run_system_once( - move |mut commands: Commands<'_, '_>, mut scene_spawner: ResMut<'_, SceneSpawner>| { - let entity = commands.spawn_empty().id(); - let id = scene_spawner.spawn_as_child(scene.clone(), entity); - (id, entity) - }, - ); - - // Check for event arrival. - app.update(); - app.world_mut().run_system_once( - move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| { - let mut events = ev_scene.read(); - - let event = events.next().expect("found no `SceneInstanceReady` event"); - assert_eq!( - event.id, scene_id, - "`SceneInstanceReady` contains the wrong `InstanceId`" - ); - assert_eq!( - event.parent, - Some(scene_entity), - "`SceneInstanceReady` contains the wrong parent entity" - ); - - assert!(events.next().is_none(), "found more than one event"); - }, - ); - } + // Spawn scene. + let scene_id = + app.world_mut() + .run_system_once(move |mut scene_spawner: ResMut<'_, SceneSpawner>| { + scene_spawner.spawn(scene.clone()) + }); - fn build_dynamic_scene(app: &mut App) -> Handle { - app.world_mut() - .run_system_once(|world: &World, asset_server: Res<'_, AssetServer>| { - asset_server.add(DynamicScene::from_world(world)) - }) + // Check trigger. + observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); } #[test] - fn event_dynamic_scene() { + fn observe_dynamic_scene() { let mut app = setup(); // Build scene. @@ -648,25 +643,32 @@ mod tests { scene_spawner.spawn_dynamic(scene.clone()) }); - // Check for event arrival. - app.update(); - app.world_mut().run_system_once( - move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| { - let mut events = ev_scene.read(); + // Check trigger. + observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + } - let event = events.next().expect("found no `SceneInstanceReady` event"); - assert_eq!( - event.id, scene_id, - "`SceneInstanceReady` contains the wrong `InstanceId`" - ); + #[test] + fn observe_scene_as_child() { + let mut app = setup(); - assert!(events.next().is_none(), "found more than one event"); + // Build scene. + let scene = build_scene(&mut app); + + // Spawn scene as child. + let (scene_id, scene_entity) = app.world_mut().run_system_once( + move |mut commands: Commands<'_, '_>, mut scene_spawner: ResMut<'_, SceneSpawner>| { + let entity = commands.spawn_empty().id(); + let id = scene_spawner.spawn_as_child(scene.clone(), entity); + (id, entity) }, ); + + // Check trigger. + observe_trigger(&mut app, scene_id, scene_entity); } #[test] - fn event_dynamic_scene_as_child() { + fn observe_dynamic_scene_as_child() { let mut app = setup(); // Build scene. @@ -681,26 +683,8 @@ mod tests { }, ); - // Check for event arrival. - app.update(); - app.world_mut().run_system_once( - move |mut ev_scene: EventReader<'_, '_, SceneInstanceReady>| { - let mut events = ev_scene.read(); - - let event = events.next().expect("found no `SceneInstanceReady` event"); - assert_eq!( - event.id, scene_id, - "`SceneInstanceReady` contains the wrong `InstanceId`" - ); - assert_eq!( - event.parent, - Some(scene_entity), - "`SceneInstanceReady` contains the wrong parent entity" - ); - - assert!(events.next().is_none(), "found more than one event"); - }, - ); + // Check trigger. + observe_trigger(&mut app, scene_id, scene_entity); } #[test] diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index b5ac7478c99c4d..497c4c4afcf507 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -3,9 +3,10 @@ use crate::{DynamicEntity, DynamicScene}; use bevy_ecs::entity::Entity; use bevy_reflect::serde::{TypedReflectDeserializer, TypedReflectSerializer}; +use bevy_reflect::PartialReflect; use bevy_reflect::{ serde::{ReflectDeserializer, TypeRegistrationDeserializer}, - Reflect, TypeRegistry, + TypeRegistry, }; use bevy_utils::HashSet; use serde::ser::SerializeMap; @@ -155,7 +156,7 @@ impl<'a> Serialize for EntitySerializer<'a> { /// deserializing through [`SceneMapDeserializer`]. pub struct SceneMapSerializer<'a> { /// List of boxed values of unique type to serialize. - pub entries: &'a [Box], + pub entries: &'a [Box], /// Type registry in which the types used in `entries` are registered. pub registry: &'a TypeRegistry, } @@ -169,7 +170,7 @@ impl<'a> Serialize for SceneMapSerializer<'a> { for reflect in self.entries { state.serialize_entry( reflect.get_represented_type_info().unwrap().type_path(), - &TypedReflectSerializer::new(&**reflect, self.registry), + &TypedReflectSerializer::new(reflect.as_partial_reflect(), self.registry), )?; } state.end() @@ -419,7 +420,7 @@ pub struct SceneMapDeserializer<'a> { } impl<'a, 'de> DeserializeSeed<'de> for SceneMapDeserializer<'a> { - type Value = Vec>; + type Value = Vec>; fn deserialize(self, deserializer: D) -> Result where @@ -436,7 +437,7 @@ struct SceneMapVisitor<'a> { } impl<'a, 'de> Visitor<'de> for SceneMapVisitor<'a> { - type Value = Vec>; + type Value = Vec>; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { formatter.write_str("map of reflect types") diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 00ece826f23235..367e7bb68ca12c 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -29,17 +29,18 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } # other -bytemuck = { version = "1.5", features = ["derive"] } +bytemuck = { version = "1", features = ["derive", "must_cast"] } fixedbitset = "0.5" guillotiere = "0.6.0" thiserror = "1.0" rectangle-pack = "0.4" bitflags = "2.3" radsort = "0.1" +nonmax = "0.5" [lints] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 38d74bb473b831..2df333539a2b50 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,7 +1,7 @@ -use crate::{Material2d, Material2dPlugin, MaterialMesh2dBundle}; +use crate::{AlphaMode2d, Material2d, Material2dPlugin, MaterialMesh2dBundle}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; -use bevy_color::{Color, ColorToComponents, LinearRgba}; +use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; use bevy_math::Vec4; use bevy_reflect::prelude::*; use bevy_render::{ @@ -46,6 +46,7 @@ impl Plugin for ColorMaterialPlugin { #[uniform(0, ColorMaterialUniform)] pub struct ColorMaterial { pub color: Color, + pub alpha_mode: AlphaMode2d, #[texture(1)] #[sampler(2)] pub texture: Option>, @@ -63,6 +64,8 @@ impl Default for ColorMaterial { ColorMaterial { color: Color::WHITE, texture: None, + // TODO should probably default to AlphaMask once supported? + alpha_mode: AlphaMode2d::Blend, } } } @@ -71,6 +74,11 @@ impl From for ColorMaterial { fn from(color: Color) -> Self { ColorMaterial { color, + alpha_mode: if color.alpha() < 1.0 { + AlphaMode2d::Blend + } else { + AlphaMode2d::Opaque + }, ..Default::default() } } @@ -89,17 +97,30 @@ impl From> for ColorMaterial { bitflags::bitflags! { #[repr(transparent)] pub struct ColorMaterialFlags: u32 { - const TEXTURE = 1 << 0; - const NONE = 0; - const UNINITIALIZED = 0xFFFF; + const TEXTURE = 1 << 0; + /// Bitmask reserving bits for the [`AlphaMode2d`] + /// Values are just sequential values bitshifted into + /// the bitmask, and can range from 0 to 3. + const ALPHA_MODE_RESERVED_BITS = Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS; + const ALPHA_MODE_OPAQUE = 0 << Self::ALPHA_MODE_SHIFT_BITS; + const ALPHA_MODE_MASK = 1 << Self::ALPHA_MODE_SHIFT_BITS; + const ALPHA_MODE_BLEND = 2 << Self::ALPHA_MODE_SHIFT_BITS; + const NONE = 0; + const UNINITIALIZED = 0xFFFF; } } +impl ColorMaterialFlags { + const ALPHA_MODE_MASK_BITS: u32 = 0b11; + const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones(); +} + /// The GPU representation of the uniform data of a [`ColorMaterial`]. #[derive(Clone, Default, ShaderType)] pub struct ColorMaterialUniform { pub color: Vec4, pub flags: u32, + pub alpha_cutoff: f32, } impl AsBindGroupShaderType for ColorMaterial { @@ -109,9 +130,20 @@ impl AsBindGroupShaderType for ColorMaterial { flags |= ColorMaterialFlags::TEXTURE; } + // Defaults to 0.5 like in 3d + let mut alpha_cutoff = 0.5; + match self.alpha_mode { + AlphaMode2d::Opaque => flags |= ColorMaterialFlags::ALPHA_MODE_OPAQUE, + AlphaMode2d::Mask(c) => { + alpha_cutoff = c; + flags |= ColorMaterialFlags::ALPHA_MODE_MASK; + } + AlphaMode2d::Blend => flags |= ColorMaterialFlags::ALPHA_MODE_BLEND, + }; ColorMaterialUniform { color: LinearRgba::from(self.color).to_f32_array().into(), flags: flags.bits(), + alpha_cutoff, } } } @@ -120,6 +152,10 @@ impl Material2d for ColorMaterial { fn fragment_shader() -> ShaderRef { COLOR_MATERIAL_SHADER_HANDLE.into() } + + fn alpha_mode(&self) -> AlphaMode2d { + self.alpha_mode + } } /// A component bundle for entities with a [`Mesh2dHandle`](crate::Mesh2dHandle) and a [`ColorMaterial`]. diff --git a/crates/bevy_sprite/src/mesh2d/color_material.wgsl b/crates/bevy_sprite/src/mesh2d/color_material.wgsl index 7bc772511681b0..a166ce453099fe 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.wgsl +++ b/crates/bevy_sprite/src/mesh2d/color_material.wgsl @@ -11,8 +11,14 @@ struct ColorMaterial { color: vec4, // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, + alpha_cutoff: f32, }; -const COLOR_MATERIAL_FLAGS_TEXTURE_BIT: u32 = 1u; + +const COLOR_MATERIAL_FLAGS_TEXTURE_BIT: u32 = 1u; +const COLOR_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3221225472u; // (0b11u32 << 30) +const COLOR_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 30) +const COLOR_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 1073741824u; // (1u32 << 30) +const COLOR_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 2147483648u; // (2u32 << 30) @group(2) @binding(0) var material: ColorMaterial; @group(2) @binding(1) var texture: texture_2d; @@ -23,14 +29,41 @@ fn fragment( mesh: VertexOutput, ) -> @location(0) vec4 { var output_color: vec4 = material.color; + #ifdef VERTEX_COLORS output_color = output_color * mesh.color; #endif + if ((material.flags & COLOR_MATERIAL_FLAGS_TEXTURE_BIT) != 0u) { output_color = output_color * textureSample(texture, texture_sampler, mesh.uv); } + + output_color = alpha_discard(material, output_color); + #ifdef TONEMAP_IN_SHADER output_color = tonemapping::tone_mapping(output_color, view.color_grading); #endif return output_color; } + +fn alpha_discard(material: ColorMaterial, output_color: vec4) -> vec4 { + var color = output_color; + let alpha_mode = material.flags & COLOR_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == COLOR_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 + color.a = 1.0; + } +#ifdef MAY_DISCARD + else if alpha_mode == COLOR_MATERIAL_FLAGS_ALPHA_MODE_MASK { + if color.a >= material.alpha_cutoff { + // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque + color.a = 1.0; + } else { + // NOTE: output_color.a < in.material.alpha_cutoff should not be rendered + discard; + } + } +#endif // MAY_DISCARD + + return color; +} \ No newline at end of file diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index d61ca002bfd10b..849fd980fbc91c 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,24 +1,26 @@ use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ - core_2d::Transparent2d, + core_2d::{AlphaMask2d, AlphaMask2dBinKey, Opaque2d, Opaque2dBinKey, Transparent2d}, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{ + entity::EntityHashMap, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; use bevy_math::FloatOrd; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::{ - mesh::{GpuMesh, MeshVertexBufferLayoutRef}, + mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{ prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, }, render_phase::{ - AddRenderCommand, DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand, - RenderCommandResult, SetItemPipeline, TrackedRenderPass, ViewSortedRenderPhases, + AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, PhaseItem, PhaseItemExtraIndex, + RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass, + ViewBinnedRenderPhases, ViewSortedRenderPhases, }, render_resource::{ AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, @@ -32,8 +34,7 @@ use bevy_render::{ }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::tracing::error; -use std::hash::Hash; -use std::marker::PhantomData; +use std::{hash::Hash, marker::PhantomData}; use crate::{ DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, @@ -121,6 +122,10 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { 0.0 } + fn alpha_mode(&self) -> AlphaMode2d { + AlphaMode2d::Opaque + } + /// Customizes the default [`RenderPipelineDescriptor`]. #[allow(unused_variables)] #[inline] @@ -133,6 +138,32 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { } } +/// Sets how a 2d material's base color alpha channel is used for transparency. +/// Currently, this only works with [`Mesh2d`](crate::mesh2d::Mesh2d). Sprites are always transparent. +/// +/// This is very similar to [`AlphaMode`](bevy_render::alpha::AlphaMode) but this only applies to 2d meshes. +/// We use a separate type because 2d doesn't support all the transparency modes that 3d does. +#[derive(Debug, Default, Reflect, Copy, Clone, PartialEq)] +#[reflect(Default, Debug)] +pub enum AlphaMode2d { + /// Base color alpha values are overridden to be fully opaque (1.0). + #[default] + Opaque, + /// Reduce transparency to fully opaque or fully transparent + /// based on a threshold. + /// + /// Compares the base color alpha value to the specified threshold. + /// If the value is below the threshold, + /// considers the color to be fully transparent (alpha is set to 0.0). + /// If it is equal to or above the threshold, + /// considers the color to be fully opaque (alpha is set to 1.0). + Mask(f32), + /// The base color alpha value defines the opacity of the color. + /// Standard alpha-blending is used to blend the fragment's color + /// with the color behind it. + Blend, +} + /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material2d`] /// asset type (which includes [`Material2d`] types). pub struct Material2dPlugin(PhantomData); @@ -153,6 +184,8 @@ where if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .add_render_command::>() + .add_render_command::>() .add_render_command::>() .init_resource::>() .init_resource::>>() @@ -338,16 +371,24 @@ impl RenderCommand

let materials = materials.into_inner(); let material_instances = material_instances.into_inner(); let Some(material_instance) = material_instances.get(&item.entity()) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(material2d) = materials.get(*material_instance) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_bind_group(I, &material2d.bind_group, &[]); RenderCommandResult::Success } } +pub const fn alpha_mode_pipeline_key(alpha_mode: AlphaMode2d) -> Mesh2dPipelineKey { + match alpha_mode { + AlphaMode2d::Blend => Mesh2dPipelineKey::BLEND_ALPHA, + AlphaMode2d::Mask(_) => Mesh2dPipelineKey::MAY_DISCARD, + _ => Mesh2dPipelineKey::NONE, + } +} + pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelineKey { match tonemapping { Tonemapping::None => Mesh2dPipelineKey::TONEMAP_METHOD_NONE, @@ -365,20 +406,24 @@ pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelin #[allow(clippy::too_many_arguments)] pub fn queue_material2d_meshes( + opaque_draw_functions: Res>, + alpha_mask_draw_functions: Res>, transparent_draw_functions: Res>, material2d_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, - msaa: Res, - render_meshes: Res>, + render_meshes: Res>, render_materials: Res>>, mut render_mesh_instances: ResMut, render_material_instances: Res>, mut transparent_render_phases: ResMut>, + mut opaque_render_phases: ResMut>, + mut alpha_mask_render_phases: ResMut>, mut views: Query<( Entity, &ExtractedView, &VisibleEntities, + &Msaa, Option<&Tonemapping>, Option<&DebandDither>, )>, @@ -389,12 +434,20 @@ pub fn queue_material2d_meshes( return; } - for (view_entity, view, visible_entities, tonemapping, dither) in &mut views { + for (view_entity, view, visible_entities, msaa, tonemapping, dither) in &mut views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; + let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else { + continue; + }; + let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view_entity) else { + continue; + }; let draw_transparent_2d = transparent_draw_functions.read().id::>(); + let draw_opaque_2d = opaque_draw_functions.read().id::>(); + let draw_alpha_mask_2d = alpha_mask_draw_functions.read().id::>(); let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); @@ -421,8 +474,9 @@ pub fn queue_material2d_meshes( let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { continue; }; - let mesh_key = - view_key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()); + let mesh_key = view_key + | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology()) + | material_2d.properties.mesh_pipeline_key_bits; let pipeline_id = pipelines.specialize( &pipeline_cache, @@ -443,21 +497,51 @@ pub fn queue_material2d_meshes( }; mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); - let mesh_z = mesh_instance.transforms.world_from_local.translation.z; - transparent_phase.add(Transparent2d { - entity: *visible_entity, - draw_function: draw_transparent_2d, - pipeline: pipeline_id, - // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the - // lowest sort key and getting closer should increase. As we have - // -z in front of the camera, the largest distance is -far with values increasing toward the - // camera. As such we can just use mesh_z as the distance - sort_key: FloatOrd(mesh_z + material_2d.depth_bias), - // Batching is done in batch_and_prepare_render_phase - batch_range: 0..1, - extra_index: PhaseItemExtraIndex::NONE, - }); + + match material_2d.properties.alpha_mode { + AlphaMode2d::Opaque => { + let bin_key = Opaque2dBinKey { + pipeline: pipeline_id, + draw_function: draw_opaque_2d, + asset_id: mesh_instance.mesh_asset_id.into(), + material_bind_group_id: material_2d.get_bind_group_id().0, + }; + opaque_phase.add( + bin_key, + *visible_entity, + BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching), + ); + } + AlphaMode2d::Mask(_) => { + let bin_key = AlphaMask2dBinKey { + pipeline: pipeline_id, + draw_function: draw_alpha_mask_2d, + asset_id: mesh_instance.mesh_asset_id.into(), + material_bind_group_id: material_2d.get_bind_group_id().0, + }; + alpha_mask_phase.add( + bin_key, + *visible_entity, + BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching), + ); + } + AlphaMode2d::Blend => { + transparent_phase.add(Transparent2d { + entity: *visible_entity, + draw_function: draw_transparent_2d, + pipeline: pipeline_id, + // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the + // lowest sort key and getting closer should increase. As we have + // -z in front of the camera, the largest distance is -far with values increasing toward the + // camera. As such we can just use mesh_z as the distance + sort_key: FloatOrd(mesh_z + material_2d.properties.depth_bias), + // Batching is done in batch_and_prepare_render_phase + batch_range: 0..1, + extra_index: PhaseItemExtraIndex::NONE, + }); + } + } } } } @@ -465,12 +549,27 @@ pub fn queue_material2d_meshes( #[derive(Component, Clone, Copy, Default, PartialEq, Eq, Deref, DerefMut)] pub struct Material2dBindGroupId(pub Option); +/// Common [`Material2d`] properties, calculated for a specific material instance. +pub struct Material2dProperties { + /// The [`AlphaMode2d`] of this material. + pub alpha_mode: AlphaMode2d, + /// Add a bias to the view depth of the mesh which can be used to force a specific render order + /// for meshes with equal depth, to avoid z-fighting. + /// The bias is in depth-texture units so large values may + pub depth_bias: f32, + /// The bits in the [`Mesh2dPipelineKey`] for this material. + /// + /// These are precalculated so that we can just "or" them together in + /// [`queue_material2d_meshes`]. + pub mesh_pipeline_key_bits: Mesh2dPipelineKey, +} + /// Data prepared for a [`Material2d`] instance. pub struct PreparedMaterial2d { pub bindings: Vec<(u32, OwnedBindingResource)>, pub bind_group: BindGroup, pub key: T::Data, - pub depth_bias: f32, + pub properties: Material2dProperties, } impl PreparedMaterial2d { @@ -492,22 +591,31 @@ impl RenderAsset for PreparedMaterial2d { fn prepare_asset( material: Self::SourceAsset, (render_device, images, fallback_image, pipeline): &mut SystemParamItem, - ) -> Result> { + ) -> Result> { match material.as_bind_group( &pipeline.material2d_layout, render_device, images, fallback_image, ) { - Ok(prepared) => Ok(PreparedMaterial2d { - bindings: prepared.bindings, - bind_group: prepared.bind_group, - key: prepared.data, - depth_bias: material.depth_bias(), - }), + Ok(prepared) => { + let mut mesh_pipeline_key_bits = Mesh2dPipelineKey::empty(); + mesh_pipeline_key_bits.insert(alpha_mode_pipeline_key(material.alpha_mode())); + Ok(PreparedMaterial2d { + bindings: prepared.bindings, + bind_group: prepared.bind_group, + key: prepared.data, + properties: Material2dProperties { + depth_bias: material.depth_bias(), + alpha_mode: material.alpha_mode(), + mesh_pipeline_key_bits, + }, + }) + } Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 16aee3f52937bb..8bce6f1cb65294 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,29 +1,35 @@ use bevy_app::Plugin; use bevy_asset::{load_internal_asset, AssetId, Handle}; -use bevy_core_pipeline::core_2d::Transparent2d; -use bevy_core_pipeline::tonemapping::{ - get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, +use bevy_core_pipeline::{ + core_2d::{AlphaMask2d, Camera2d, Opaque2d, Transparent2d, CORE_2D_DEPTH_FORMAT}, + tonemapping::{ + get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, + }, }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{ + entity::EntityHashMap, prelude::*, query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_math::{Affine3, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::batching::gpu_preprocessing::IndirectParameters; +use bevy_render::batching::no_gpu_preprocessing::batch_and_prepare_binned_render_phase; use bevy_render::batching::no_gpu_preprocessing::{ self, batch_and_prepare_sorted_render_phase, write_batched_instance_buffer, BatchedInstanceBuffer, }; -use bevy_render::mesh::{GpuMesh, MeshVertexBufferLayoutRef}; +use bevy_render::batching::GetFullBatchData; +use bevy_render::mesh::allocator::MeshAllocator; +use bevy_render::mesh::{MeshVertexBufferLayoutRef, RenderMesh}; use bevy_render::texture::FallbackImage; use bevy_render::{ batching::{GetBatchData, NoAutomaticBatching}, globals::{GlobalsBuffer, GlobalsUniform}, - mesh::{GpuBufferInfo, Mesh}, + mesh::{Mesh, RenderMeshBufferInfo}, render_asset::RenderAssets, render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::{binding_types::uniform_buffer, *}, @@ -37,6 +43,8 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::GlobalTransform; +use bevy_utils::tracing::error; +use nonmax::NonMaxU32; use crate::Material2dBindGroupId; @@ -106,6 +114,10 @@ impl Plugin for Mesh2dRenderPlugin { .add_systems( Render, ( + batch_and_prepare_binned_render_phase:: + .in_set(RenderSet::PrepareResources), + batch_and_prepare_binned_render_phase:: + .in_set(RenderSet::PrepareResources), batch_and_prepare_sorted_render_phase:: .in_set(RenderSet::PrepareResources), write_batched_instance_buffer:: @@ -160,7 +172,7 @@ pub struct Mesh2dTransforms { pub flags: u32, } -#[derive(ShaderType, Clone)] +#[derive(ShaderType, Clone, Copy)] pub struct Mesh2dUniform { // Affine 4x3 matrix transposed to 3x4 pub world_from_local: [Vec4; 3], @@ -357,12 +369,16 @@ impl Mesh2dPipeline { } impl GetBatchData for Mesh2dPipeline { - type Param = SRes; + type Param = ( + SRes, + SRes>, + SRes, + ); type CompareData = (Material2dBindGroupId, AssetId); type BufferData = Mesh2dUniform; fn get_batch_data( - mesh_instances: &SystemParamItem, + (mesh_instances, _, _): &SystemParamItem, entity: Entity, ) -> Option<(Self::BufferData, Option)> { let mesh_instance = mesh_instances.get(&entity)?; @@ -376,6 +392,81 @@ impl GetBatchData for Mesh2dPipeline { } } +impl GetFullBatchData for Mesh2dPipeline { + type BufferInputData = (); + + fn get_binned_batch_data( + (mesh_instances, _, _): &SystemParamItem, + entity: Entity, + ) -> Option { + let mesh_instance = mesh_instances.get(&entity)?; + Some((&mesh_instance.transforms).into()) + } + + fn get_index_and_compare_data( + _: &SystemParamItem, + _query_item: Entity, + ) -> Option<(NonMaxU32, Option)> { + error!( + "`get_index_and_compare_data` is only intended for GPU mesh uniform building, \ + but this is not yet implemented for 2d meshes" + ); + None + } + + fn get_binned_index( + _: &SystemParamItem, + _query_item: Entity, + ) -> Option { + error!( + "`get_binned_index` is only intended for GPU mesh uniform building, \ + but this is not yet implemented for 2d meshes" + ); + None + } + + fn get_batch_indirect_parameters_index( + (mesh_instances, meshes, mesh_allocator): &SystemParamItem, + indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::IndirectParametersBuffer, + entity: Entity, + instance_index: u32, + ) -> Option { + let mesh_instance = mesh_instances.get(&entity)?; + let mesh = meshes.get(mesh_instance.mesh_asset_id)?; + let vertex_buffer_slice = mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id)?; + + // Note that `IndirectParameters` covers both of these structures, even + // though they actually have distinct layouts. See the comment above that + // type for more information. + let indirect_parameters = match mesh.buffer_info { + RenderMeshBufferInfo::Indexed { + count: index_count, .. + } => { + let index_buffer_slice = + mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id)?; + IndirectParameters { + vertex_or_index_count: index_count, + instance_count: 0, + first_vertex_or_first_index: index_buffer_slice.range.start, + base_vertex_or_first_instance: vertex_buffer_slice.range.start, + first_instance: instance_index, + } + } + RenderMeshBufferInfo::NonIndexed => IndirectParameters { + vertex_or_index_count: mesh.vertex_count, + instance_count: 0, + first_vertex_or_first_index: vertex_buffer_slice.range.start, + base_vertex_or_first_instance: instance_index, + first_instance: instance_index, + }, + }; + + (indirect_parameters_buffer.push(indirect_parameters) as u32) + .try_into() + .ok() + } +} + bitflags::bitflags! { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[repr(transparent)] @@ -387,6 +478,8 @@ bitflags::bitflags! { const HDR = 1 << 0; const TONEMAP_IN_SHADER = 1 << 1; const DEBAND_DITHER = 1 << 2; + const BLEND_ALPHA = 1 << 3; + const MAY_DISCARD = 1 << 4; 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; @@ -531,6 +624,10 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { } } + if key.contains(Mesh2dPipelineKey::MAY_DISCARD) { + shader_defs.push("MAY_DISCARD".into()); + } + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; let format = match key.contains(Mesh2dPipelineKey::HDR) { @@ -538,6 +635,17 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { false => TextureFormat::bevy_default(), }; + let (depth_write_enabled, label, blend); + if key.contains(Mesh2dPipelineKey::BLEND_ALPHA) { + label = "transparent_mesh2d_pipeline"; + blend = Some(BlendState::ALPHA_BLENDING); + depth_write_enabled = false; + } else { + label = "opaque_mesh2d_pipeline"; + blend = None; + depth_write_enabled = true; + } + Ok(RenderPipelineDescriptor { vertex: VertexState { shader: MESH2D_SHADER_HANDLE, @@ -551,7 +659,7 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { format, - blend: Some(BlendState::ALPHA_BLENDING), + blend, write_mask: ColorWrites::ALL, })], }), @@ -566,13 +674,28 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { topology: key.primitive_topology(), strip_index_format: None, }, - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.msaa_samples(), mask: !0, alpha_to_coverage_enabled: false, }, - label: Some("transparent_mesh2d_pipeline".into()), + label: Some(label.into()), }) } } @@ -610,7 +733,7 @@ pub fn prepare_mesh2d_view_bind_groups( render_device: Res, mesh2d_pipeline: Res, view_uniforms: Res, - views: Query<(Entity, &Tonemapping), With>, + views: Query<(Entity, &Tonemapping), (With, With)>, globals_buffer: Res, tonemapping_luts: Res, images: Res>, @@ -694,7 +817,11 @@ impl RenderCommand

for SetMesh2dBindGroup { pub struct DrawMesh2d; impl RenderCommand

for DrawMesh2d { - type Param = (SRes>, SRes); + type Param = ( + SRes>, + SRes, + SRes, + ); type ViewQuery = (); type ItemQuery = (); @@ -703,34 +830,47 @@ impl RenderCommand

for DrawMesh2d { item: &P, _view: (), _item_query: Option<()>, - (meshes, render_mesh2d_instances): SystemParamItem<'w, '_, Self::Param>, + (meshes, render_mesh2d_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let meshes = meshes.into_inner(); let render_mesh2d_instances = render_mesh2d_instances.into_inner(); + let mesh_allocator = mesh_allocator.into_inner(); let Some(RenderMesh2dInstance { mesh_asset_id, .. }) = render_mesh2d_instances.get(&item.entity()) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(gpu_mesh) = meshes.get(*mesh_asset_id) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; + }; + let Some(vertex_buffer_slice) = mesh_allocator.mesh_vertex_slice(mesh_asset_id) else { + return RenderCommandResult::Skip; }; - pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); let batch_range = item.batch_range(); match &gpu_mesh.buffer_info { - GpuBufferInfo::Indexed { - buffer, + RenderMeshBufferInfo::Indexed { index_format, count, } => { - pass.set_index_buffer(buffer.slice(..), 0, *index_format); - pass.draw_indexed(0..*count, 0, batch_range.clone()); + let Some(index_buffer_slice) = mesh_allocator.mesh_index_slice(mesh_asset_id) + else { + return RenderCommandResult::Skip; + }; + + pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); + + pass.draw_indexed( + index_buffer_slice.range.start..(index_buffer_slice.range.start + count), + vertex_buffer_slice.range.start as i32, + batch_range.clone(), + ); } - GpuBufferInfo::NonIndexed => { + RenderMeshBufferInfo::NonIndexed => { pass.draw(0..gpu_mesh.vertex_count, batch_range.clone()); } } diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 1009c0c268d32c..83633a95e2e961 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -1,7 +1,7 @@ use crate::{Material2d, Material2dKey, Material2dPlugin, Mesh2dHandle}; use bevy_app::{Plugin, Startup, Update}; use bevy_asset::{load_internal_asset, Asset, Assets, Handle}; -use bevy_color::{LinearRgba, Srgba}; +use bevy_color::{Color, LinearRgba}; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; use bevy_render::{ @@ -67,7 +67,7 @@ pub struct Wireframe2d; #[derive(Component, Debug, Clone, Default, Reflect)] #[reflect(Component, Default)] pub struct Wireframe2dColor { - pub color: Srgba, + pub color: Color, } /// Disables wireframe rendering for any entity it is attached to. @@ -81,13 +81,13 @@ pub struct NoWireframe2d; #[derive(Resource, Debug, Clone, Default, ExtractResource, Reflect)] #[reflect(Resource)] pub struct Wireframe2dConfig { - /// Whether to show wireframes for all meshes. + /// Whether to show wireframes for all 2D meshes. /// Can be overridden for individual meshes by adding a [`Wireframe2d`] or [`NoWireframe2d`] component. pub global: bool, /// If [`Self::global`] is set, any [`Entity`] that does not have a [`Wireframe2d`] component attached to it will have /// wireframes using this color. Otherwise, this will be the fallback color for any entity that has a [`Wireframe2d`], /// but no [`Wireframe2dColor`]. - pub default_color: Srgba, + pub default_color: Color, } #[derive(Resource)] diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index d60efb0977eb2a..60c64131fe12b0 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -7,7 +7,7 @@ use crate::{ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ - core_2d::Transparent2d, + core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, tonemapping::{ get_lut_bind_group_layout_entries, get_lut_bindings, DebandDither, Tonemapping, TonemappingLuts, @@ -294,7 +294,25 @@ impl SpecializedRenderPipeline for SpritePipeline { topology: PrimitiveTopology::TriangleList, strip_index_format: None, }, - depth_stencil: None, + // Sprites are always alpha blended so they never need to write to depth. + // They just need to read it in case an opaque mesh2d + // that wrote to depth is present. + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.msaa_samples(), mask: !0, @@ -472,26 +490,25 @@ pub fn queue_sprites( sprite_pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - msaa: Res, extracted_sprites: Res, mut transparent_render_phases: ResMut>, mut views: Query<( Entity, &VisibleEntities, &ExtractedView, + &Msaa, Option<&Tonemapping>, Option<&DebandDither>, )>, ) { - let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples()); - let draw_sprite_function = draw_functions.read().id::(); - for (view_entity, visible_entities, view, tonemapping, dither) in &mut views { + for (view_entity, visible_entities, view, msaa, tonemapping, dither) in &mut views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; + let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples()); let mut view_key = SpritePipelineKey::from_hdr(view.hdr) | msaa_key; if !view.hdr { @@ -805,7 +822,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou ) -> RenderCommandResult { let image_bind_groups = image_bind_groups.into_inner(); let Some(batch) = batch else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_bind_group( @@ -835,7 +852,7 @@ impl RenderCommand

for DrawSpriteBatch { ) -> RenderCommandResult { let sprite_meta = sprite_meta.into_inner(); let Some(batch) = batch else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_index_buffer( diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index ae57eb760c0911..a0ce5071ecf19e 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -31,6 +31,16 @@ pub struct Sprite { pub anchor: Anchor, } +impl Sprite { + /// Create a Sprite with a custom size + pub fn sized(custom_size: Vec2) -> Self { + Sprite { + custom_size: Some(custom_size), + ..Default::default() + } + } +} + /// Controls how the image is altered when scaled. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component)] diff --git a/crates/bevy_sprite/src/texture_slice/slicer.rs b/crates/bevy_sprite/src/texture_slice/slicer.rs index 2b69f6fc1f38d7..b290272edd70d2 100644 --- a/crates/bevy_sprite/src/texture_slice/slicer.rs +++ b/crates/bevy_sprite/src/texture_slice/slicer.rs @@ -126,11 +126,11 @@ impl TextureSlicer { ), }, draw_size: vec2( - bl_corner.draw_size.x, - render_size.y - (bl_corner.draw_size.y + tl_corner.draw_size.y), + tl_corner.draw_size.x, + render_size.y - (tl_corner.draw_size.y + bl_corner.draw_size.y), ), offset: vec2( - -render_size.x + bl_corner.draw_size.x, + tl_corner.draw_size.x - render_size.x, bl_corner.draw_size.y - tl_corner.draw_size.y, ) / 2.0, }, @@ -141,21 +141,21 @@ impl TextureSlicer { base_rect.max.x - self.border.right, base_rect.min.y + self.border.top, ), - max: vec2(base_rect.max.x, base_rect.max.y - self.border.bottom), + max: base_rect.max - vec2(0.0, self.border.bottom), }, draw_size: vec2( - br_corner.draw_size.x, - render_size.y - (br_corner.draw_size.y + tr_corner.draw_size.y), + tr_corner.draw_size.x, + render_size.y - (tr_corner.draw_size.y + br_corner.draw_size.y), ), offset: vec2( - render_size.x - br_corner.draw_size.x, + render_size.x - tr_corner.draw_size.x, br_corner.draw_size.y - tr_corner.draw_size.y, ) / 2.0, }, ] } - /// Computes the 2 vertical side slices (bottom and top borders) + /// Computes the 2 vertical side slices (top and bottom borders) #[must_use] fn vertical_side_slices( &self, @@ -164,24 +164,6 @@ impl TextureSlicer { 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( - (bl_corner.draw_size.x - br_corner.draw_size.x) / 2.0, - bl_corner.offset.y, - ), - }, // Top TextureSlice { texture_rect: Rect { @@ -196,9 +178,27 @@ impl TextureSlicer { tl_corner.draw_size.y, ), offset: vec2( - (tl_corner.draw_size.x - tr_corner.draw_size.x) / 2.0, - tl_corner.offset.y, + tl_corner.draw_size.x - tr_corner.draw_size.x, + render_size.y - tl_corner.draw_size.y, + ) / 2.0, + }, + // Bottom + TextureSlice { + texture_rect: Rect { + min: vec2( + base_rect.min.x + self.border.left, + base_rect.max.y - self.border.bottom, + ), + max: base_rect.max - vec2(self.border.right, 0.0), + }, + draw_size: vec2( + render_size.x - (bl_corner.draw_size.x + br_corner.draw_size.x), + bl_corner.draw_size.y, ), + offset: vec2( + bl_corner.draw_size.x - br_corner.draw_size.x, + bl_corner.draw_size.y - render_size.y, + ) / 2.0, }, ] } @@ -216,11 +216,8 @@ impl TextureSlicer { #[must_use] pub fn compute_slices(&self, rect: Rect, render_size: Option) -> Vec { let render_size = render_size.unwrap_or_else(|| rect.size()); - let rect_size = rect.size() / 2.0; - if self.border.left >= rect_size.x - || self.border.right >= rect_size.x - || self.border.top >= rect_size.y - || self.border.bottom >= rect_size.y + if self.border.left + self.border.right >= rect.size().x + || self.border.top + self.border.bottom >= rect.size().y { bevy_utils::tracing::error!( "TextureSlicer::border has out of bounds values. No slicing will be applied" @@ -234,7 +231,7 @@ impl TextureSlicer { let mut slices = Vec::with_capacity(9); // Corners are in this order: [TL, TR, BL, BR] let corners = self.corner_slices(rect, render_size); - // Vertical Sides: [B, T] + // Vertical Sides: [T, B] let vertical_sides = self.vertical_side_slices(&corners, rect, render_size); // Horizontal Sides: [L, R] let horizontal_sides = self.horizontal_side_slices(&corners, rect, render_size); @@ -242,19 +239,13 @@ impl TextureSlicer { let center = TextureSlice { texture_rect: Rect { min: rect.min + vec2(self.border.left, self.border.top), - max: vec2( - rect.max.x - self.border.right, - rect.max.y - self.border.bottom, - ), + max: rect.max - vec2(self.border.right, self.border.bottom), }, 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::new( - (corners[0].draw_size.x - corners[3].draw_size.x) / 2.0, - (corners[2].draw_size.y - corners[0].draw_size.y) / 2.0, + render_size.x - (corners[0].draw_size.x + corners[1].draw_size.x), + render_size.y - (corners[0].draw_size.y + corners[2].draw_size.y), ), + offset: vec2(vertical_sides[0].offset.x, horizontal_sides[0].offset.y), }; slices.extend(corners); @@ -399,7 +390,7 @@ mod test { } ); assert_eq!( - vertical_sides[1], /* top */ + vertical_sides[0], /* top */ TextureSlice { texture_rect: Rect { min: Vec2::new(5.0, 0.0), diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml index f4332984bb2f2f..d8dafee87d16df 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -28,5 +28,5 @@ bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev", optional workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options"] all-features = true diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml index d7ed0ae60c2f6e..50a4d468b1508f 100644 --- a/crates/bevy_state/macros/Cargo.toml +++ b/crates/bevy_state/macros/Cargo.toml @@ -19,5 +19,5 @@ proc-macro2 = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options"] all-features = true diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index 710185f2a240d4..f550e8daa09d74 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -1,9 +1,5 @@ -use bevy_app::{App, MainScheduleOrder, Plugin, PreUpdate, Startup, SubApp}; -use bevy_ecs::{ - event::Events, - schedule::{IntoSystemConfigs, ScheduleLabel}, - world::FromWorld, -}; +use bevy_app::{App, MainScheduleOrder, Plugin, PreStartup, PreUpdate, SubApp}; +use bevy_ecs::{event::Events, schedule::IntoSystemConfigs, world::FromWorld}; use bevy_utils::{tracing::warn, warn_once}; use crate::state::{ @@ -12,6 +8,9 @@ use crate::state::{ }; use crate::state_scoped::clear_state_scoped_entities; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{FromReflect, GetTypeRegistration, Typed}; + /// State installation methods for [`App`] and [`SubApp`]. pub trait AppExtStates { /// Initializes a [`State`] with standard starting values. @@ -27,6 +26,8 @@ pub trait AppExtStates { /// /// Note that you can also apply state transitions at other points in the schedule /// by triggering the [`StateTransition`](struct@StateTransition) schedule manually. + /// + /// The use of any states requires the presence of [`StatesPlugin`] (which is included in `DefaultPlugins`). fn init_state(&mut self) -> &mut Self; /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously @@ -57,6 +58,25 @@ pub trait AppExtStates { /// /// For more information refer to [`StateScoped`](crate::state_scoped::StateScoped). fn enable_state_scoped_entities(&mut self) -> &mut Self; + + #[cfg(feature = "bevy_reflect")] + /// Registers the state type `T` using [`App::register_type`], + /// and adds [`ReflectState`](crate::reflect::ReflectState) type data to `T` in the type registry. + /// + /// This enables reflection code to access the state. For detailed information, see the docs on [`crate::reflect::ReflectState`] . + fn register_type_state(&mut self) -> &mut Self + where + S: States + FromReflect + GetTypeRegistration + Typed; + + #[cfg(feature = "bevy_reflect")] + /// Registers the state type `T` using [`App::register_type`], + /// and adds [`crate::reflect::ReflectState`] and [`crate::reflect::ReflectFreelyMutableState`] type data to `T` in the type registry. + /// + /// This enables reflection code to access and modify the state. + /// For detailed information, see the docs on [`crate::reflect::ReflectState`] and [`crate::reflect::ReflectFreelyMutableState`]. + fn register_type_mutable_state(&mut self) -> &mut Self + where + S: FreelyMutableState + FromReflect + GetTypeRegistration + Typed; } /// Separate function to only warn once for all state installation methods. @@ -73,7 +93,9 @@ impl AppExtStates for SubApp { self.init_resource::>() .init_resource::>() .add_event::>(); - let schedule = self.get_schedule_mut(StateTransition).unwrap(); + let schedule = self.get_schedule_mut(StateTransition).expect( + "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling init_state?" + ); S::register_state(schedule); let state = self.world().resource::>().get().clone(); self.world_mut().send_event(StateTransitionEvent { @@ -94,7 +116,9 @@ impl AppExtStates for SubApp { self.insert_resource::>(State::new(state.clone())) .init_resource::>() .add_event::>(); - let schedule = self.get_schedule_mut(StateTransition).unwrap(); + let schedule = self.get_schedule_mut(StateTransition).expect( + "The `StateTransition` schedule is missing. Did you forget to add StatesPlugin or DefaultPlugins before calling init_state?" + ); S::register_state(schedule); self.world_mut().send_event(StateTransitionEvent { exited: None, @@ -181,6 +205,30 @@ impl AppExtStates for SubApp { clear_state_scoped_entities::.in_set(StateTransitionSteps::ExitSchedules), ) } + + #[cfg(feature = "bevy_reflect")] + fn register_type_state(&mut self) -> &mut Self + where + S: States + FromReflect + GetTypeRegistration + Typed, + { + self.register_type::(); + self.register_type::>(); + self.register_type_data::(); + self + } + + #[cfg(feature = "bevy_reflect")] + fn register_type_mutable_state(&mut self) -> &mut Self + where + S: FreelyMutableState + FromReflect + GetTypeRegistration + Typed, + { + self.register_type::(); + self.register_type::>(); + self.register_type::>(); + self.register_type_data::(); + self.register_type_data::(); + self + } } impl AppExtStates for App { @@ -208,16 +256,36 @@ impl AppExtStates for App { self.main_mut().enable_state_scoped_entities::(); self } + + #[cfg(feature = "bevy_reflect")] + fn register_type_state(&mut self) -> &mut Self + where + S: States + FromReflect + GetTypeRegistration + Typed, + { + self.main_mut().register_type_state::(); + self + } + + #[cfg(feature = "bevy_reflect")] + fn register_type_mutable_state(&mut self) -> &mut Self + where + S: FreelyMutableState + FromReflect + GetTypeRegistration + Typed, + { + self.main_mut().register_type_mutable_state::(); + self + } } /// Registers the [`StateTransition`] schedule in the [`MainScheduleOrder`] to enable state processing. +#[derive(Default)] pub struct StatesPlugin; impl Plugin for StatesPlugin { fn build(&self, app: &mut App) { let mut schedule = app.world_mut().resource_mut::(); schedule.insert_after(PreUpdate, StateTransition); - setup_state_transitions_in_world(app.world_mut(), Some(Startup.intern())); + schedule.insert_startup_before(PreStartup, StateTransition); + setup_state_transitions_in_world(app.world_mut()); } } diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index a8f578e8c7a025..5506221ce35cf7 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -27,6 +27,10 @@ //! - The [`in_state`](crate::condition::in_state) and [`state_changed`](crate::condition::state_changed) run conditions - which are used //! to determine whether a system should run based on the current state. +// `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +#![allow(internal_features)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] + #[cfg(feature = "bevy_app")] /// Provides [`App`](bevy_app::App) and [`SubApp`](bevy_app::SubApp) with state installation methods pub mod app; @@ -39,6 +43,10 @@ pub mod state; /// [`clear_state_scoped_entities`](crate::state_scoped::clear_state_scoped_entities) for managing lifetime of entities. pub mod state_scoped; +#[cfg(feature = "bevy_reflect")] +/// Provides definitions for the basic traits required by the state system +pub mod reflect; + /// Most commonly used re-exported types. pub mod prelude { #[cfg(feature = "bevy_app")] @@ -46,6 +54,9 @@ pub mod prelude { pub use crate::app::AppExtStates; #[doc(hidden)] pub use crate::condition::*; + #[cfg(feature = "bevy_app")] + #[doc(hidden)] + pub use crate::reflect::{ReflectFreelyMutableState, ReflectState}; #[doc(hidden)] pub use crate::state::{ last_transition, ComputedStates, EnterSchedules, ExitSchedules, NextState, OnEnter, OnExit, diff --git a/crates/bevy_state/src/reflect.rs b/crates/bevy_state/src/reflect.rs new file mode 100644 index 00000000000000..422d4aff1a341b --- /dev/null +++ b/crates/bevy_state/src/reflect.rs @@ -0,0 +1,165 @@ +use crate::state::{FreelyMutableState, NextState, State, States}; + +use bevy_ecs::reflect::from_reflect_with_fallback; +use bevy_ecs::world::World; +use bevy_reflect::{FromType, Reflect, TypePath, TypeRegistry}; + +/// A struct used to operate on the reflected [`States`] trait of a type. +/// +/// A [`ReflectState`] for type `T` can be obtained via +/// [`bevy_reflect::TypeRegistration::data`]. +#[derive(Clone)] +pub struct ReflectState(ReflectStateFns); + +/// The raw function pointers needed to make up a [`ReflectState`]. +#[derive(Clone)] +pub struct ReflectStateFns { + /// Function pointer implementing [`ReflectState::reflect()`]. + pub reflect: fn(&World) -> Option<&dyn Reflect>, +} + +impl ReflectStateFns { + /// Get the default set of [`ReflectStateFns`] for a specific component 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 ReflectState { + /// Gets the value of this [`States`] type from the world as a reflected reference. + pub fn reflect<'a>(&self, world: &'a World) -> Option<&'a dyn Reflect> { + (self.0.reflect)(world) + } +} + +impl FromType for ReflectState { + fn from_type() -> Self { + ReflectState(ReflectStateFns { + reflect: |world| { + world + .get_resource::>() + .map(|res| res.get() as &dyn Reflect) + }, + }) + } +} + +/// A struct used to operate on the reflected [`FreelyMutableState`] trait of a type. +/// +/// A [`ReflectFreelyMutableState`] for type `T` can be obtained via +/// [`bevy_reflect::TypeRegistration::data`]. +#[derive(Clone)] +pub struct ReflectFreelyMutableState(ReflectFreelyMutableStateFns); + +/// The raw function pointers needed to make up a [`ReflectFreelyMutableState`]. +#[derive(Clone)] +pub struct ReflectFreelyMutableStateFns { + /// Function pointer implementing [`ReflectFreelyMutableState::set_next_state()`]. + pub set_next_state: fn(&mut World, &dyn Reflect, &TypeRegistry), +} + +impl ReflectFreelyMutableStateFns { + /// Get the default set of [`ReflectFreelyMutableStateFns`] for a specific component 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 ReflectFreelyMutableState { + /// Tentatively set a pending state transition to a reflected [`ReflectFreelyMutableState`]. + pub fn set_next_state(&self, world: &mut World, state: &dyn Reflect, registry: &TypeRegistry) { + (self.0.set_next_state)(world, state, registry); + } +} + +impl FromType for ReflectFreelyMutableState { + fn from_type() -> Self { + ReflectFreelyMutableState(ReflectFreelyMutableStateFns { + set_next_state: |world, reflected_state, registry| { + let new_state: S = from_reflect_with_fallback( + reflected_state.as_partial_reflect(), + world, + registry, + ); + if let Some(mut next_state) = world.get_resource_mut::>() { + next_state.set(new_state); + } + }, + }) + } +} + +#[cfg(test)] +mod tests { + use crate as bevy_state; + use crate::app::{AppExtStates, StatesPlugin}; + use crate::reflect::{ReflectFreelyMutableState, ReflectState}; + use crate::state::State; + use bevy_app::App; + use bevy_ecs::prelude::AppTypeRegistry; + use bevy_reflect::Reflect; + use bevy_state_macros::States; + use std::any::TypeId; + + #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, States, Reflect)] + enum StateTest { + A, + B, + } + + #[test] + fn test_reflect_state_operations() { + let mut app = App::new(); + app.add_plugins(StatesPlugin) + .insert_state(StateTest::A) + .register_type_mutable_state::(); + + let type_registry = app.world_mut().resource::().0.clone(); + let type_registry = type_registry.read(); + + let (reflect_state, reflect_mutable_state) = ( + type_registry + .get_type_data::(TypeId::of::()) + .unwrap() + .clone(), + type_registry + .get_type_data::(TypeId::of::()) + .unwrap() + .clone(), + ); + + let current_value = reflect_state.reflect(app.world()).unwrap(); + assert_eq!( + current_value.downcast_ref::().unwrap(), + &StateTest::A + ); + + reflect_mutable_state.set_next_state(app.world_mut(), &StateTest::B, &type_registry); + + assert_ne!( + app.world().resource::>().get(), + &StateTest::B + ); + + app.update(); + + assert_eq!( + app.world().resource::>().get(), + &StateTest::B + ); + + let current_value = reflect_state.reflect(app.world()).unwrap(); + assert_eq!( + current_value.downcast_ref::().unwrap(), + &StateTest::B + ); + } +} diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs index 5220b9f9152adc..5e5ea005df0d74 100644 --- a/crates/bevy_state/src/state/mod.rs +++ b/crates/bevy_state/src/state/mod.rs @@ -19,7 +19,6 @@ pub use transitions::*; mod tests { use bevy_ecs::event::EventRegistry; use bevy_ecs::prelude::*; - use bevy_ecs::schedule::ScheduleLabel; use bevy_state_macros::States; use bevy_state_macros::SubStates; @@ -64,7 +63,7 @@ mod tests { world.insert_resource(schedules); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); world.run_schedule(StateTransition); assert_eq!(world.resource::>().0, SimpleState::A); @@ -120,7 +119,7 @@ mod tests { world.insert_resource(schedules); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); world.run_schedule(StateTransition); assert_eq!(world.resource::>().0, SimpleState::A); @@ -180,7 +179,7 @@ mod tests { world.insert_resource(schedules); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); world.run_schedule(StateTransition); assert_eq!(world.resource::>().0, SimpleState::A); @@ -275,7 +274,7 @@ mod tests { world.insert_resource(schedules); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); world.run_schedule(StateTransition); assert_eq!(world.resource::>().0, SimpleState::A); @@ -354,9 +353,6 @@ mod tests { } } - #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] - struct Startup; - #[test] fn computed_state_transitions_are_produced_correctly() { let mut world = World::new(); @@ -367,7 +363,7 @@ mod tests { world.init_resource::>(); world.init_resource::(); - setup_state_transitions_in_world(&mut world, Some(Startup.intern())); + setup_state_transitions_in_world(&mut world); let mut schedules = world .get_resource_mut::() @@ -431,7 +427,7 @@ mod tests { world.init_resource::(); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); assert_eq!(world.resource::>().0, SimpleState::A); assert_eq!(world.resource::>().0, SimpleState2::A1); @@ -508,7 +504,7 @@ mod tests { #[test] fn same_state_transition_should_emit_event_and_not_run_schedules() { let mut world = World::new(); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); EventRegistry::register_event::>(&mut world); world.init_resource::>(); let mut schedules = world.resource_mut::(); @@ -568,7 +564,7 @@ mod tests { SubState::register_sub_state_systems(&mut apply_changes); schedules.insert(apply_changes); world.insert_resource(schedules); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); world.insert_resource(NextState::Pending(SimpleState::B(true))); world.run_schedule(StateTransition); @@ -599,7 +595,7 @@ mod tests { TestComputedState::register_computed_state_systems(&mut apply_changes); schedules.insert(apply_changes); world.insert_resource(schedules); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); world.insert_resource(NextState::Pending(SimpleState::B(true))); world.run_schedule(StateTransition); @@ -651,7 +647,7 @@ mod tests { #[test] fn check_transition_orders() { let mut world = World::new(); - setup_state_transitions_in_world(&mut world, None); + setup_state_transitions_in_world(&mut world); EventRegistry::register_event::>(&mut world); EventRegistry::register_event::>(&mut world); EventRegistry::register_event::>( diff --git a/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs index abc95ab5a41289..c9f4279634c38c 100644 --- a/crates/bevy_state/src/state/resources.rs +++ b/crates/bevy_state/src/state/resources.rs @@ -11,6 +11,9 @@ use super::{freely_mutable_state::FreelyMutableState, states::States}; #[cfg(feature = "bevy_reflect")] use bevy_ecs::prelude::ReflectResource; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::prelude::ReflectDefault; + /// A finite-state machine whose transitions have associated schedules /// ([`OnEnter(state)`](crate::state::OnEnter) and [`OnExit(state)`](crate::state::OnExit)). /// @@ -115,7 +118,7 @@ impl Deref for State { #[cfg_attr( feature = "bevy_reflect", derive(bevy_reflect::Reflect), - reflect(Resource) + reflect(Resource, Default) )] pub enum NextState { /// No state transition is pending diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index 6b83b2d17cb503..0ba8caf1227a08 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -27,7 +27,7 @@ mod sealed { /// /// It is sealed, and auto implemented for all [`States`] types and /// tuples containing them. -pub trait StateSet: sealed::StateSetSealed { +pub trait StateSet: StateSetSealed { /// The total [`DEPENDENCY_DEPTH`](`States::DEPENDENCY_DEPTH`) of all /// the states that are part of this [`StateSet`], added together. /// @@ -229,10 +229,12 @@ impl StateSet for S { } macro_rules! impl_state_set_sealed_tuples { - ($(($param: ident, $val: ident, $evt: ident)), *) => { - impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} + ($(#[$meta:meta])* $(($param: ident, $val: ident, $evt: ident)), *) => { + $(#[$meta])* + impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} - impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { + $(#[$meta])* + impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { const SET_DEPENDENCY_DEPTH : usize = $($param::DEPENDENCY_DEPTH +)* 0; @@ -338,4 +340,12 @@ macro_rules! impl_state_set_sealed_tuples { }; } -all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader); +all_tuples!( + #[doc(fake_variadic)] + impl_state_set_sealed_tuples, + 1, + 15, + S, + s, + ereader +); diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index 0b510e0a2fed9e..08f3d254acd282 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -2,9 +2,7 @@ use std::{marker::PhantomData, mem}; use bevy_ecs::{ event::{Event, EventReader, EventWriter}, - schedule::{ - InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel, Schedules, SystemSet, - }, + schedule::{IntoSystemSetConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, }; @@ -182,10 +180,7 @@ pub(crate) fn internal_apply_state_transition( /// /// Runs automatically when using `App` to insert states, but needs to /// be added manually in other situations. -pub fn setup_state_transitions_in_world( - world: &mut World, - startup_label: Option, -) { +pub fn setup_state_transitions_in_world(world: &mut World) { let mut schedules = world.get_resource_or_insert_with(Schedules::default); if schedules.contains(StateTransition) { return; @@ -201,12 +196,6 @@ pub fn setup_state_transitions_in_world( .chain(), ); schedules.insert(schedule); - - if let Some(startup) = startup_label { - schedules.add_systems(startup, |world: &mut World| { - let _ = world.try_run_schedule(StateTransition); - }); - } } /// Returns the latest state transition event of type `S`, if any are available. diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 5a8ea9ff6b0efe..0e8831e59f9c95 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -20,6 +20,8 @@ concurrent-queue = { version = "2.0.0", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen-futures = "0.4" +pin-project = "1" +futures-channel = "0.3" [dev-dependencies] web-time = { version = "1.1" } @@ -28,5 +30,5 @@ web-time = { version = "1.1" } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_tasks/README.md b/crates/bevy_tasks/README.md index 1d1a7fb90465b4..91ac95dce8a278 100644 --- a/crates/bevy_tasks/README.md +++ b/crates/bevy_tasks/README.md @@ -22,7 +22,7 @@ It is based on [`async-executor`][async-executor], a lightweight executor that a In order to be able to optimize task execution in multi-threaded environments, bevy provides three different thread pools via which tasks of different kinds can be spawned. (The same API is used in single-threaded environments, even if execution is limited to a single thread. -This currently applies to WASM targets.) +This currently applies to Wasm targets.) The determining factor for what kind of work should go in each pool is latency requirements: * For CPU-intensive work (tasks that generally spin until completion) we have a standard diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 17cfb348ef2c56..3eb33c6603e70b 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -8,7 +8,9 @@ mod slice; pub use slice::{ParallelSlice, ParallelSliceMut}; +#[cfg_attr(target_arch = "wasm32", path = "wasm_task.rs")] mod task; + pub use task::Task; #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] @@ -19,7 +21,7 @@ pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] mod single_threaded_task_pool; #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] -pub use single_threaded_task_pool::{FakeTask, Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; +pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; mod usages; #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index de7a13891593df..cd6bbc79d63adb 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -1,6 +1,8 @@ use std::sync::Arc; use std::{cell::RefCell, future::Future, marker::PhantomData, mem, rc::Rc}; +use crate::Task; + thread_local! { static LOCAL_EXECUTOR: async_executor::LocalExecutor<'static> = const { async_executor::LocalExecutor::new() }; } @@ -145,34 +147,33 @@ impl TaskPool { .collect() } - /// Spawns a static future onto the thread pool. The returned Task is a future. It can also be - /// cancelled and "detached" allowing it to continue running without having to be polled by the + /// Spawns a static future onto the thread pool. The returned Task is a future, which can be polled + /// to retrieve the output of the original future. Dropping the task will attempt to cancel it. + /// It can also be "detached", allowing it to continue running without having to be polled by the /// end-user. /// /// If the provided future is non-`Send`, [`TaskPool::spawn_local`] should be used instead. - pub fn spawn(&self, future: impl Future + 'static) -> FakeTask + pub fn spawn(&self, future: impl Future + 'static) -> Task where T: 'static, { #[cfg(target_arch = "wasm32")] - wasm_bindgen_futures::spawn_local(async move { - future.await; - }); + return Task::wrap_future(future); #[cfg(not(target_arch = "wasm32"))] { LOCAL_EXECUTOR.with(|executor| { - let _task = executor.spawn(future); + let task = executor.spawn(future); // Loop until all tasks are done while executor.try_tick() {} - }); - } - FakeTask + Task::new(task) + }) + } } /// Spawns a static future on the JS event loop. This is exactly the same as [`TaskPool::spawn`]. - pub fn spawn_local(&self, future: impl Future + 'static) -> FakeTask + pub fn spawn_local(&self, future: impl Future + 'static) -> Task where T: 'static, { @@ -198,17 +199,6 @@ impl TaskPool { } } -/// An empty task used in single-threaded contexts. -/// -/// This does nothing and is therefore safe, and recommended, to ignore. -#[derive(Debug)] -pub struct FakeTask; - -impl FakeTask { - /// No op on the single threaded task pool - pub fn detach(self) {} -} - /// A `TaskPool` scope for running one or more non-`'static` futures. /// /// For more information, see [`TaskPool::scope`]. diff --git a/crates/bevy_tasks/src/wasm_task.rs b/crates/bevy_tasks/src/wasm_task.rs new file mode 100644 index 00000000000000..47c082516ad2b0 --- /dev/null +++ b/crates/bevy_tasks/src/wasm_task.rs @@ -0,0 +1,82 @@ +use std::{ + any::Any, + future::{Future, IntoFuture}, + panic::{AssertUnwindSafe, UnwindSafe}, + pin::Pin, + task::Poll, +}; + +use futures_channel::oneshot; + +/// Wraps an asynchronous task, a spawned future. +/// +/// Tasks are also futures themselves and yield the output of the spawned future. +#[derive(Debug)] +pub struct Task(oneshot::Receiver>); + +impl Task { + pub(crate) fn wrap_future(future: impl Future + 'static) -> Self { + let (sender, receiver) = oneshot::channel(); + wasm_bindgen_futures::spawn_local(async move { + // Catch any panics that occur when polling the future so they can + // be propagated back to the task handle. + let value = CatchUnwind(AssertUnwindSafe(future)).await; + let _ = sender.send(value); + }); + Self(receiver.into_future()) + } + + /// When building for Wasm, this method has no effect. + /// This is only included for feature parity with other platforms. + pub fn detach(self) {} + + /// Requests a task to be cancelled and returns a future that suspends until it completes. + /// Returns the output of the future if it has already completed. + /// + /// # Implementation + /// + /// When building for Wasm, it is not possible to cancel tasks, which means this is the same + /// as just awaiting the task. This method is only included for feature parity with other platforms. + pub async fn cancel(self) -> Option { + match self.0.await { + Ok(Ok(value)) => Some(value), + Err(_) => None, + Ok(Err(panic)) => { + // drop this to prevent the panic payload from resuming the panic on drop. + // this also leaks the box but I'm not sure how to avoid that + std::mem::forget(panic); + None + } + } + } +} + +impl Future for Task { + type Output = T; + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + match Pin::new(&mut self.0).poll(cx) { + Poll::Ready(Ok(Ok(value))) => Poll::Ready(value), + // NOTE: Propagating the panic here sorta has parity with the async_executor behavior. + // For those tasks, polling them after a panic returns a `None` which gets `unwrap`ed, so + // using `resume_unwind` here is essentially keeping the same behavior while adding more information. + Poll::Ready(Ok(Err(panic))) => std::panic::resume_unwind(panic), + Poll::Ready(Err(_)) => panic!("Polled a task after it was cancelled"), + Poll::Pending => Poll::Pending, + } + } +} + +type Panic = Box; + +#[pin_project::pin_project] +struct CatchUnwind(#[pin] F); + +impl Future for CatchUnwind { + type Output = Result; + fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context) -> Poll { + std::panic::catch_unwind(AssertUnwindSafe(|| self.project().0.poll(cx)))?.map(Ok) + } +} diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 2b6f61d9b340eb..a5a337c676d56a 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -42,5 +42,5 @@ approx = "0.5.1" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index d5c4527e0b11d6..8922f0f1032ef7 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -71,6 +71,10 @@ use bevy_render::{ }; use bevy_sprite::SpriteSystem; +/// The raw data for the default font used by `bevy_text` +#[cfg(feature = "default_font")] +pub const DEFAULT_FONT_DATA: &[u8] = include_bytes!("FiraMono-subset.ttf"); + /// Adds text rendering support to an app. /// /// When the `bevy_text` feature is enabled with the `bevy` crate, this @@ -109,7 +113,7 @@ impl Plugin for TextPlugin { .in_set(VisibilitySystems::CalculateBounds) .after(update_text2d_layout), update_text2d_layout - .after(font_atlas_set::remove_dropped_font_atlas_sets) + .after(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 325460b8b1ab51..6568a9204462ea 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -304,7 +304,7 @@ pub struct TextMeasureInfo { pub min: Vec2, /// Maximum size for a text area in pixels, to be used when laying out widgets with taffy pub max: Vec2, - buffer: cosmic_text::Buffer, + buffer: Buffer, } impl std::fmt::Debug for TextMeasureInfo { diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 39723a06ee4136..9172b6896e96b6 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -128,6 +128,7 @@ impl Text { /// Contains the value of the text in a section and how it should be styled. #[derive(Debug, Default, Clone, Reflect)] +#[reflect(Default)] pub struct TextSection { /// The content (in `String` form) of the text in the section. pub value: String, @@ -153,7 +154,6 @@ impl TextSection { } } -#[cfg(feature = "default_font")] impl From<&str> for TextSection { fn from(value: &str) -> Self { Self { @@ -163,7 +163,6 @@ impl From<&str> for TextSection { } } -#[cfg(feature = "default_font")] impl From for TextSection { fn from(value: String) -> Self { Self { @@ -217,7 +216,8 @@ pub struct TextStyle { /// If the `font` is not specified, then /// * if `default_font` feature is enabled (enabled by default in `bevy` crate), /// `FiraMono-subset.ttf` compiled into the library is used. - /// * otherwise no text will be rendered. + /// * otherwise no text will be rendered, unless a custom font is loaded into the default font + /// handle. pub font: Handle, /// The vertical height of rasterized glyphs in the font atlas in pixels. /// diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 2982aa41c5c57d..93d14281198d05 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -32,5 +32,5 @@ thiserror = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_time/src/common_conditions.rs b/crates/bevy_time/src/common_conditions.rs index 760ddf84ffa34f..e00acf8cffb8b4 100644 --- a/crates/bevy_time/src/common_conditions.rs +++ b/crates/bevy_time/src/common_conditions.rs @@ -134,7 +134,7 @@ pub fn once_after_delay(duration: Duration) -> impl FnMut(Res

for SetUiViewBindGroup { ui_meta: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { - pass.set_bind_group( - I, - ui_meta.into_inner().view_bind_group.as_ref().unwrap(), - &[view_uniform.offset], - ); + let Some(view_bind_group) = ui_meta.into_inner().view_bind_group.as_ref() else { + return RenderCommandResult::Failure("view_bind_group not available"); + }; + pass.set_bind_group(I, view_bind_group, &[view_uniform.offset]); RenderCommandResult::Success } } @@ -192,7 +194,7 @@ impl RenderCommand

for SetUiTextureBindGroup ) -> RenderCommandResult { let image_bind_groups = image_bind_groups.into_inner(); let Some(batch) = batch else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]); @@ -214,15 +216,21 @@ impl RenderCommand

for DrawUiNode { pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(batch) = batch else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; - let ui_meta = ui_meta.into_inner(); + let Some(vertices) = ui_meta.vertices.buffer() else { + return RenderCommandResult::Failure("missing vertices to draw ui"); + }; + let Some(indices) = ui_meta.indices.buffer() else { + return RenderCommandResult::Failure("missing indices to draw ui"); + }; + // Store the vertices - pass.set_vertex_buffer(0, ui_meta.vertices.buffer().unwrap().slice(..)); + pass.set_vertex_buffer(0, vertices.slice(..)); // Define how to "connect" the vertices pass.set_index_buffer( - ui_meta.indices.buffer().unwrap().slice(..), + indices.slice(..), 0, bevy_render::render_resource::IndexFormat::Uint32, ); diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 49eee2ed255793..2eaa2f583bc4da 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -292,10 +292,10 @@ impl RenderCommand

pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(material_handle) = material_handle else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(material) = materials.into_inner().get(material_handle.material) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_bind_group(I, &material.bind_group, &[]); RenderCommandResult::Success @@ -317,7 +317,7 @@ impl RenderCommand

for DrawUiMaterialNode { pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(batch) = batch else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..)); @@ -624,6 +624,7 @@ impl RenderAsset for PreparedUiMaterial { Err(AsBindGroupError::RetryNextUpdate) => { Err(PrepareAssetError::RetryNextUpdate(material)) } + Err(other) => Err(PrepareAssetError::AsBindGroupError(other)), } } } diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index f7e1a16ec7c5f5..6b0ed38f1c186b 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -18,7 +18,6 @@ use crate::{CalculatedClip, ExtractedUiNode, Node, NodeType, UiImage}; #[derive(Debug, Clone, Component)] pub struct ComputedTextureSlices { slices: Vec, - image_size: Vec2, } impl ComputedTextureSlices { @@ -56,7 +55,6 @@ impl ComputedTextureSlices { let mut rect = slice.texture_rect; rect.min *= scale; rect.max *= scale; - let atlas_size = Some(self.image_size * scale); ExtractedUiNode { stack_index: node.stack_index, color: image.color.into(), @@ -65,7 +63,7 @@ impl ComputedTextureSlices { flip_x, flip_y, image: image.texture.id(), - atlas_size, + atlas_scaling: Some(scale), clip: clip.map(|clip| clip.clip), camera_entity, border: [0.; 4], @@ -99,13 +97,10 @@ fn compute_texture_slices( atlas: Option<&TextureAtlas>, atlas_layouts: &Assets, ) -> Option { - let (image_size, texture_rect) = match atlas { + let texture_rect = match atlas { Some(a) => { let layout = atlas_layouts.get(&a.layout)?; - ( - layout.size.as_vec2(), - layout.textures.get(a.index)?.as_rect(), - ) + layout.textures.get(a.index)?.as_rect() } None => { let image = images.get(&image_handle.texture)?; @@ -113,11 +108,10 @@ fn compute_texture_slices( image.texture_descriptor.size.width as f32, image.texture_descriptor.size.height as f32, ); - let rect = Rect { + Rect { min: Vec2::ZERO, max: size, - }; - (size, rect) + } } }; let slices = match scale_mode { @@ -135,7 +129,7 @@ fn compute_texture_slices( slice.tiled(*stretch_value, (*tile_x, *tile_y)) } }; - Some(ComputedTextureSlices { slices, image_size }) + Some(ComputedTextureSlices { slices }) } /// System reacting to added or modified [`Image`] handles, and recompute sprite slices diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 5e918e4d1ca8eb..fb285823b37987 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -173,13 +173,6 @@ pub struct Style { /// pub overflow: Overflow, - /// Defines the text direction. For example, English is written LTR (left-to-right) while Arabic is written RTL (right-to-left). - /// - /// Note: the corresponding CSS property also affects box layout order, but this isn't yet implemented in Bevy. - /// - /// - pub direction: Direction, - /// The horizontal position of the left edge of the node. /// - For relatively positioned nodes, this is relative to the node's position as computed during regular layout. /// - For absolutely positioned nodes, this is relative to the *parent* node's bounding box. @@ -435,7 +428,6 @@ impl Style { right: Val::Auto, top: Val::Auto, bottom: Val::Auto, - direction: Direction::DEFAULT, flex_direction: FlexDirection::DEFAULT, flex_wrap: FlexWrap::DEFAULT, align_items: AlignItems::DEFAULT, @@ -730,35 +722,6 @@ impl Default for JustifyContent { } } -/// Defines the text direction. -/// -/// For example, English is written LTR (left-to-right) while Arabic is written RTL (right-to-left). -#[derive(Copy, Clone, PartialEq, Eq, Debug, Reflect)] -#[reflect(Default, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub enum Direction { - /// Inherit from parent node. - Inherit, - /// Text is written left to right. - LeftToRight, - /// Text is written right to left. - RightToLeft, -} - -impl Direction { - pub const DEFAULT: Self = Self::Inherit; -} - -impl Default for Direction { - fn default() -> Self { - Self::DEFAULT - } -} - /// Defines the layout model used by this node. /// /// Part of the [`Style`] component. @@ -1007,12 +970,12 @@ impl Default for GridAutoFlow { } } -#[derive(Copy, Clone, PartialEq, Debug, Reflect)] -#[reflect_value(PartialEq)] +#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)] +#[reflect(Default, PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), - reflect_value(Serialize, Deserialize) + reflect(Serialize, Deserialize) )] pub enum MinTrackSizingFunction { /// Track minimum size should be a fixed pixel value @@ -1024,6 +987,7 @@ pub enum MinTrackSizingFunction { /// Track minimum size should be content sized under a max-content constraint MaxContent, /// Track minimum size should be automatically sized + #[default] Auto, /// Track minimum size should be a percent of the viewport's smaller dimension. VMin(f32), @@ -1035,12 +999,12 @@ pub enum MinTrackSizingFunction { Vw(f32), } -#[derive(Copy, Clone, PartialEq, Debug, Reflect)] -#[reflect_value(PartialEq)] +#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)] +#[reflect(Default, PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), - reflect_value(Serialize, Deserialize) + reflect(Serialize, Deserialize) )] pub enum MaxTrackSizingFunction { /// Track maximum size should be a fixed pixel value @@ -1056,6 +1020,7 @@ pub enum MaxTrackSizingFunction { /// Track maximum size should be sized according to the fit-content formula with a percentage limit FitContentPercent(f32), /// Track maximum size should be automatically sized + #[default] Auto, /// The dimension as a fraction of the total available grid space (`fr` units in CSS) /// Specified value is the numerator of the fraction. Denominator is the sum of all fractions specified in that grid dimension. @@ -1234,7 +1199,7 @@ impl Default for GridTrack { } #[derive(Copy, Clone, PartialEq, Debug, Reflect)] -#[reflect(PartialEq)] +#[reflect(Default, PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), @@ -1257,6 +1222,12 @@ pub enum GridTrackRepetition { AutoFit, } +impl Default for GridTrackRepetition { + fn default() -> Self { + Self::Count(1) + } +} + impl From for GridTrackRepetition { fn from(count: u16) -> Self { Self::Count(count) @@ -1289,7 +1260,7 @@ impl From for GridTrackRepetition { /// then all tracks (in and outside of the repetition) must be fixed size (px or percent). Integer repetitions are just shorthand for writing out /// N tracks longhand and are not subject to the same limitations. #[derive(Clone, PartialEq, Debug, Reflect)] -#[reflect(PartialEq)] +#[reflect(Default, PartialEq)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), @@ -1446,6 +1417,15 @@ impl RepeatedGridTrack { } } +impl Default for RepeatedGridTrack { + fn default() -> Self { + Self { + repetition: Default::default(), + tracks: SmallVec::from_buf([GridTrack::default()]), + } + } +} + impl From for RepeatedGridTrack { fn from(track: GridTrack) -> Self { Self { @@ -1457,10 +1437,7 @@ impl From for RepeatedGridTrack { impl From for Vec { fn from(track: GridTrack) -> Self { - vec![GridTrack { - min_sizing_function: track.min_sizing_function, - max_sizing_function: track.max_sizing_function, - }] + vec![track] } } @@ -1988,7 +1965,7 @@ impl Default for ZIndex { /// /// #[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] -#[reflect(PartialEq, Default)] +#[reflect(Component, PartialEq, Default)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 7f0d3e3ae87da1..52f995961a9dfb 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -15,7 +15,7 @@ detailed_trace = [] ahash = "0.8.7" tracing = { version = "0.1", default-features = false, features = ["std"] } web-time = { version = "1.1" } -hashbrown = { version = "0.14", features = ["serde"] } +hashbrown = { version = "0.14.2", features = ["serde"] } bevy_utils_proc_macros = { version = "0.15.0-dev", path = "macros" } thread_local = "1.0" @@ -29,5 +29,5 @@ getrandom = { version = "0.2.0", features = ["js"] } workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_utils/macros/Cargo.toml b/crates/bevy_utils/macros/Cargo.toml index ec7066087b8528..6c2b41a798cdc5 100644 --- a/crates/bevy_utils/macros/Cargo.toml +++ b/crates/bevy_utils/macros/Cargo.toml @@ -18,5 +18,5 @@ proc-macro2 = "1.0" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_utils/macros/src/lib.rs b/crates/bevy_utils/macros/src/lib.rs index 3fdd2997671b37..4e539ebe435fab 100644 --- a/crates/bevy_utils/macros/src/lib.rs +++ b/crates/bevy_utils/macros/src/lib.rs @@ -3,14 +3,17 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] use proc_macro::TokenStream; +use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream}, parse_macro_input, + spanned::Spanned as _, token::Comma, - Ident, LitInt, Result, + Attribute, Error, Ident, LitInt, LitStr, Result, }; struct AllTuples { + fake_variadic: bool, macro_ident: Ident, start: usize, end: usize, @@ -19,6 +22,7 @@ struct AllTuples { impl Parse for AllTuples { fn parse(input: ParseStream) -> Result { + let fake_variadic = input.call(parse_fake_variadic_attr)?; let macro_ident = input.parse::()?; input.parse::()?; let start = input.parse::()?.base10_parse()?; @@ -30,7 +34,15 @@ impl Parse for AllTuples { idents.push(input.parse::()?); } + if start > 1 && fake_variadic { + return Err(Error::new( + input.span(), + "#[doc(fake_variadic)] only works when the tuple with length one is included", + )); + } + Ok(AllTuples { + fake_variadic, macro_ident, start, end, @@ -42,15 +54,19 @@ impl Parse for AllTuples { /// Helper macro to generate tuple pyramids. Useful to generate scaffolding to work around Rust /// lacking variadics. Invoking `all_tuples!(impl_foo, start, end, P, Q, ..)` /// invokes `impl_foo` providing ident tuples through arity `start..=end`. +/// If you require the length of the tuple, see [`all_tuples_with_size!`]. +/// /// # Examples -/// A single parameter. -/// ``` -/// use std::marker::PhantomData; -/// use bevy_utils_proc_macros::all_tuples; /// +/// ## Single parameter +/// +/// ``` +/// # use std::marker::PhantomData; +/// # use bevy_utils_proc_macros::all_tuples; +/// # /// struct Foo { /// // .. -/// _phantom: PhantomData +/// # _phantom: PhantomData /// } /// /// trait WrappedInFoo { @@ -72,10 +88,12 @@ impl Parse for AllTuples { /// // .. /// // impl_wrapped_in_foo!(T0 .. T14); /// ``` -/// Multiple parameters. -/// ``` -/// use bevy_utils_proc_macros::all_tuples; /// +/// # Multiple parameters +/// +/// ``` +/// # use bevy_utils_proc_macros::all_tuples; +/// # /// trait Append { /// type Out; /// fn append(tup: Self, item: Item) -> Self::Out; @@ -105,7 +123,42 @@ impl Parse for AllTuples { /// // impl_append!((P0, p0), (P1, p1), (P2, p2)); /// // .. /// // impl_append!((P0, p0) .. (P14, p14)); -/// ```` +/// ``` +/// +/// **`#[doc(fake_variadic)]`** +/// +/// To improve the readability of your docs when implementing a trait for +/// tuples or fn pointers of varying length you can use the rustdoc-internal `fake_variadic` marker. +/// All your impls are collapsed and shown as a single `impl Trait for (F₁, F₂, …, Fₙ)`. +/// +/// The `all_tuples!` macro does most of the work for you, the only change to your implementation macro +/// is that you have to accept attributes using `$(#[$meta:meta])*`. +/// +/// Since this feature requires a nightly compiler, it's only enabled on docs.rs by default. +/// Add the following to your lib.rs if not already present: +/// +/// ``` +/// // `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +/// #![allow(internal_features)] +/// #![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] +/// ``` +/// +/// ``` +/// # use bevy_utils_proc_macros::all_tuples; +/// # +/// trait Variadic {} +/// +/// impl Variadic for () {} +/// +/// macro_rules! impl_variadic { +/// ($(#[$meta:meta])* $(($P:ident, $p:ident)),*) => { +/// $(#[$meta])* +/// impl<$($P),*> Variadic for ($($P,)*) {} +/// } +/// } +/// +/// all_tuples!(#[doc(fake_variadic)] impl_variadic, 1, 15, P, p); +/// ``` #[proc_macro] pub fn all_tuples(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as AllTuples); @@ -116,22 +169,19 @@ pub fn all_tuples(input: TokenStream) -> TokenStream { .idents .iter() .map(|ident| format_ident!("{}{}", ident, i)); - if input.idents.len() < 2 { - ident_tuples.push(quote! { - #(#idents)* - }); - } else { - ident_tuples.push(quote! { - (#(#idents),*) - }); - } + ident_tuples.push(to_ident_tuple(idents, input.idents.len())); } let macro_ident = &input.macro_ident; let invocations = (input.start..=input.end).map(|i| { - let ident_tuples = &ident_tuples[..i]; + let ident_tuples = choose_ident_tuples(&input, &ident_tuples, i); + let attrs = if input.fake_variadic { + fake_variadic_attrs(len, i) + } else { + TokenStream2::default() + }; quote! { - #macro_ident!(#(#ident_tuples),*); + #macro_ident!(#attrs #ident_tuples); } }); TokenStream::from(quote! { @@ -141,6 +191,116 @@ pub fn all_tuples(input: TokenStream) -> TokenStream { }) } +/// Helper macro to generate tuple pyramids with their length. Useful to generate scaffolding to +/// work around Rust lacking variadics. Invoking `all_tuples_with_size!(impl_foo, start, end, P, Q, ..)` +/// invokes `impl_foo` providing ident tuples through arity `start..=end` preceded by their length. +/// If you don't require the length of the tuple, see [`all_tuples!`]. +/// +/// # Examples +/// +/// ## Single parameter +/// +/// ``` +/// # use std::marker::PhantomData; +/// # use bevy_utils_proc_macros::all_tuples_with_size; +/// # +/// struct Foo { +/// // .. +/// # _phantom: PhantomData +/// } +/// +/// trait WrappedInFoo { +/// type Tup; +/// const LENGTH: usize; +/// } +/// +/// macro_rules! impl_wrapped_in_foo { +/// ($N:expr, $($T:ident),*) => { +/// impl<$($T),*> WrappedInFoo for ($($T,)*) { +/// type Tup = ($(Foo<$T>,)*); +/// const LENGTH: usize = $N; +/// } +/// }; +/// } +/// +/// all_tuples_with_size!(impl_wrapped_in_foo, 0, 15, T); +/// // impl_wrapped_in_foo!(0); +/// // impl_wrapped_in_foo!(1, T0); +/// // impl_wrapped_in_foo!(2, T0, T1); +/// // .. +/// // impl_wrapped_in_foo!(15, T0 .. T14); +/// ``` +/// +/// ## Multiple parameters +/// +/// ``` +/// # use bevy_utils_proc_macros::all_tuples_with_size; +/// # +/// trait Append { +/// type Out; +/// fn append(tup: Self, item: Item) -> Self::Out; +/// } +/// +/// impl Append for () { +/// type Out = (Item,); +/// fn append(_: Self, item: Item) -> Self::Out { +/// (item,) +/// } +/// } +/// +/// macro_rules! impl_append { +/// ($N:expr, $(($P:ident, $p:ident)),*) => { +/// impl<$($P),*> Append for ($($P,)*) { +/// type Out = ($($P),*, Item); +/// fn append(($($p,)*): Self, item: Item) -> Self::Out { +/// ($($p),*, item) +/// } +/// } +/// } +/// } +/// +/// all_tuples_with_size!(impl_append, 1, 15, P, p); +/// // impl_append!(1, (P0, p0)); +/// // impl_append!(2, (P0, p0), (P1, p1)); +/// // impl_append!(3, (P0, p0), (P1, p1), (P2, p2)); +/// // .. +/// // impl_append!(15, (P0, p0) .. (P14, p14)); +/// ``` +/// +/// **`#[doc(fake_variadic)]`** +/// +/// To improve the readability of your docs when implementing a trait for +/// tuples or fn pointers of varying length you can use the rustdoc-internal `fake_variadic` marker. +/// All your impls are collapsed and shown as a single `impl Trait for (F₁, F₂, …, Fₙ)`. +/// +/// The `all_tuples!` macro does most of the work for you, the only change to your implementation macro +/// is that you have to accept attributes using `$(#[$meta:meta])*`. +/// +/// Since this feature requires a nightly compiler, it's only enabled on docs.rs by default. +/// Add the following to your lib.rs if not already present: +/// +/// ``` +/// // `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +/// #![allow(internal_features)] +/// #![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] +/// ``` +/// +/// ``` +/// # use bevy_utils_proc_macros::all_tuples_with_size; +/// # +/// trait Variadic {} +/// +/// impl Variadic for () {} +/// +/// macro_rules! impl_variadic { +/// ($N:expr, $(#[$meta:meta])* $(($P:ident, $p:ident)),*) => { +/// $(#[$meta])* +/// impl<$($P),*> Variadic for ($($P,)*) {} +/// } +/// } +/// +/// all_tuples_with_size!(#[doc(fake_variadic)] impl_variadic, 1, 15, P, p); +/// ``` #[proc_macro] pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as AllTuples); @@ -151,17 +311,8 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { .idents .iter() .map(|ident| format_ident!("{}{}", ident, i)); - if input.idents.len() < 2 { - ident_tuples.push(quote! { - #(#idents)* - }); - } else { - ident_tuples.push(quote! { - (#(#idents),*) - }); - } + ident_tuples.push(to_ident_tuple(idents, input.idents.len())); } - let macro_ident = &input.macro_ident; let invocations = (input.start..=input.end).map(|i| { let ident_tuples = &ident_tuples[..i]; @@ -175,3 +326,72 @@ pub fn all_tuples_with_size(input: TokenStream) -> TokenStream { )* }) } + +/// Parses the attribute `#[doc(fake_variadic)]` +fn parse_fake_variadic_attr(input: ParseStream) -> Result { + let attribute = match input.call(Attribute::parse_outer)? { + attributes if attributes.is_empty() => return Ok(false), + attributes if attributes.len() == 1 => attributes[0].clone(), + attributes => { + return Err(Error::new( + input.span(), + format!("Expected exactly one attribute, got {}", attributes.len()), + )) + } + }; + + if attribute.path().is_ident("doc") { + let nested = attribute.parse_args::()?; + if nested == "fake_variadic" { + return Ok(true); + } + } + + Err(Error::new( + attribute.meta.span(), + "Unexpected attribute".to_string(), + )) +} + +fn choose_ident_tuples(input: &AllTuples, ident_tuples: &[TokenStream2], i: usize) -> TokenStream2 { + // `rustdoc` uses the first ident to generate nice + // idents with subscript numbers e.g. (F₁, F₂, …, Fₙ). + // We don't want two numbers, so we use the + // original, unnumbered idents for this case. + if input.fake_variadic && i == 1 { + let ident_tuple = to_ident_tuple(input.idents.iter().cloned(), input.idents.len()); + quote! { #ident_tuple } + } else { + let ident_tuples = &ident_tuples[..i]; + quote! { #(#ident_tuples),* } + } +} + +fn to_ident_tuple(idents: impl Iterator, len: usize) -> TokenStream2 { + if len < 2 { + quote! { #(#idents)* } + } else { + quote! { (#(#idents),*) } + } +} + +fn fake_variadic_attrs(len: usize, i: usize) -> TokenStream2 { + let cfg = quote! { any(docsrs, docsrs_dep) }; + match i { + // An empty tuple (i.e. the unit type) is still documented separately, + // so no `#[doc(hidden)]` here. + 0 => TokenStream2::default(), + // The `#[doc(fake_variadic)]` attr has to be on the first impl block. + 1 => { + let doc = LitStr::new( + &format!("This trait is implemented for tuples up to {len} items long."), + Span2::call_site(), + ); + quote! { + #[cfg_attr(#cfg, doc(fake_variadic))] + #[cfg_attr(#cfg, doc = #doc)] + } + } + _ => quote! { #[cfg_attr(#cfg, doc(hidden))] }, + } +} diff --git a/crates/bevy_utils/src/cow_arc.rs b/crates/bevy_utils/src/cow_arc.rs index 47eb7c05429b5c..635d31a583ef6d 100644 --- a/crates/bevy_utils/src/cow_arc.rs +++ b/crates/bevy_utils/src/cow_arc.rs @@ -26,6 +26,18 @@ pub enum CowArc<'a, T: ?Sized + 'static> { Owned(Arc), } +impl CowArc<'static, T> { + /// Indicates this [`CowArc`] should have a static lifetime. + /// This ensures if this was created with a value `Borrowed(&'static T)`, it is replaced with `Static(&'static T)`. + #[inline] + pub fn as_static(self) -> Self { + match self { + Self::Borrowed(value) | Self::Static(value) => Self::Static(value), + Self::Owned(value) => Self::Owned(value), + } + } +} + impl<'a, T: ?Sized> Deref for CowArc<'a, T> { type Target = T; diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index e804313da77fb2..a37a6e18ba3fce 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -49,7 +49,7 @@ use std::{ #[cfg(not(target_arch = "wasm32"))] mod conditional_send { - /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. WASM), + /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm), /// futures aren't Send. pub trait ConditionalSend: Send {} impl ConditionalSend for T {} @@ -64,7 +64,7 @@ mod conditional_send { pub use conditional_send::*; -/// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. WASM), +/// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. Wasm), /// futures aren't Send. pub trait ConditionalSendFuture: std::future::Future + ConditionalSend {} impl ConditionalSendFuture for T {} @@ -376,7 +376,7 @@ pub struct NoOpHasher(u64); // This is for types that already contain a high-quality hash and want to skip // re-hashing that hash. -impl std::hash::Hasher for NoOpHasher { +impl Hasher for NoOpHasher { fn finish(&self) -> u64 { self.0 } @@ -506,7 +506,7 @@ mod tests { fn write_u64(&mut self, _: u64) {} } - std::hash::Hash::hash(&TypeId::of::<()>(), &mut Hasher); + Hash::hash(&TypeId::of::<()>(), &mut Hasher); } #[test] diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index 516c8915fb56fa..6159c6ec946987 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -9,8 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = [] -serialize = ["serde", "smol_str/serde"] +serialize = ["serde", "smol_str/serde", "bevy_ecs/serialize"] [dependencies] # bevy @@ -33,5 +32,5 @@ smol_str = "0.2" workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 9dd91bd86219dd..f27af26699424a 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -15,17 +15,19 @@ use std::sync::{Arc, Mutex}; use bevy_a11y::Focus; -mod cursor; mod event; +mod monitor; mod raw_handle; mod system; +mod system_cursor; mod window; pub use crate::raw_handle::*; -pub use cursor::*; pub use event::*; +pub use monitor::*; pub use system::*; +pub use system_cursor::*; pub use window::*; #[allow(missing_docs)] @@ -33,7 +35,7 @@ pub mod prelude { #[allow(deprecated)] #[doc(hidden)] pub use crate::{ - CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection, + CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection, ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition, WindowResizeConstraints, }; diff --git a/crates/bevy_window/src/monitor.rs b/crates/bevy_window/src/monitor.rs new file mode 100644 index 00000000000000..64fafa89c127d7 --- /dev/null +++ b/crates/bevy_window/src/monitor.rs @@ -0,0 +1,69 @@ +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::{IVec2, UVec2}; +use bevy_reflect::Reflect; + +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// Represents an available monitor as reported by the user's operating system, which can be used +/// to query information about the display, such as its size, position, and video modes. +/// +/// Each monitor corresponds to an entity and can be used to position a monitor using +/// [`crate::window::MonitorSelection::Entity`]. +/// +/// # Warning +/// +/// This component is synchronized with `winit` through `bevy_winit`, but is effectively +/// read-only as `winit` does not support changing monitor properties. +#[derive(Component, Debug, Clone, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Component)] +pub struct Monitor { + /// The name of the monitor + pub name: Option, + /// The height of the monitor in physical pixels + pub physical_height: u32, + /// The width of the monitor in physical pixels + pub physical_width: u32, + /// The position of the monitor in physical pixels + pub physical_position: IVec2, + /// The refresh rate of the monitor in millihertz + pub refresh_rate_millihertz: Option, + /// The scale factor of the monitor + pub scale_factor: f64, + /// The video modes that the monitor supports + pub video_modes: Vec, +} + +/// A marker component for the primary monitor +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component)] +pub struct PrimaryMonitor; + +impl Monitor { + /// Returns the physical size of the monitor in pixels + pub fn physical_size(&self) -> UVec2 { + UVec2::new(self.physical_width, self.physical_height) + } +} + +/// Represents a video mode that a monitor supports +#[derive(Debug, Clone, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct VideoMode { + /// The resolution of the video mode + pub physical_size: UVec2, + /// The bit depth of the video mode + pub bit_depth: u16, + /// The refresh rate in millihertz + pub refresh_rate_millihertz: u32, +} diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index d6bb40bfefdc67..d507e9e84139e9 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -50,6 +50,9 @@ pub fn close_when_requested( } // Mark the window as closing so we can despawn it on the next frame for event in closed.read() { - commands.entity(event.window).insert(ClosingWindow); + // When spamming the window close button on windows (other platforms too probably) + // we may receive a `WindowCloseRequested` for a window we've just despawned in the above + // loop. + commands.entity(event.window).try_insert(ClosingWindow); } } diff --git a/crates/bevy_window/src/cursor.rs b/crates/bevy_window/src/system_cursor.rs similarity index 97% rename from crates/bevy_window/src/cursor.rs rename to crates/bevy_window/src/system_cursor.rs index 3a68b7ee929a0c..b3865c4a35c586 100644 --- a/crates/bevy_window/src/cursor.rs +++ b/crates/bevy_window/src/system_cursor.rs @@ -73,7 +73,7 @@ use bevy_reflect::{prelude::ReflectDefault, Reflect}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// The icon to display for a [`Window`](crate::window::Window)'s [`Cursor`](crate::window::Cursor). +/// The icon to display for a window. /// /// Examples of all of these cursors can be found [here](https://www.w3schools.com/cssref/playit.php?filename=playcss_cursor&preval=crosshair). /// This `enum` is simply a copy of a similar `enum` found in [`winit`](https://docs.rs/winit/latest/winit/window/enum.CursorIcon.html). @@ -89,7 +89,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; reflect(Serialize, Deserialize) )] #[reflect(Debug, PartialEq, Default)] -pub enum CursorIcon { +pub enum SystemCursorIcon { /// The platform-dependent default cursor. Often rendered as arrow. #[default] Default, @@ -107,7 +107,7 @@ pub enum CursorIcon { Pointer, /// A progress indicator. The program is performing some processing, but is - /// different from [`CursorIcon::Wait`] in that the user may still interact + /// different from [`SystemCursorIcon::Wait`] in that the user may still interact /// with the program. Progress, diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 0e1a441b832643..dfaa0ecdae36b5 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -12,8 +12,6 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_utils::tracing::warn; -use crate::CursorIcon; - /// Marker [`Component`] for the window considered the primary window. /// /// Currently this is assumed to only exist on 1 entity at a time. @@ -107,16 +105,16 @@ impl NormalizedWindowRef { /// /// Because this component is synchronized with `winit`, it can be used to perform /// OS-integrated windowing operations. For example, here's a simple system -/// to change the cursor type: +/// to change the window mode: /// /// ``` /// # use bevy_ecs::query::With; /// # use bevy_ecs::system::Query; -/// # use bevy_window::{CursorIcon, PrimaryWindow, Window}; -/// fn change_cursor(mut windows: Query<&mut Window, With>) { +/// # use bevy_window::{WindowMode, PrimaryWindow, Window, MonitorSelection}; +/// fn change_window_mode(mut windows: Query<&mut Window, With>) { /// // Query returns one window typically. /// for mut window in windows.iter_mut() { -/// window.cursor.icon = CursorIcon::Wait; +/// window.mode = WindowMode::Fullscreen(MonitorSelection::Current); /// } /// } /// ``` @@ -128,8 +126,9 @@ impl NormalizedWindowRef { )] #[reflect(Component, Default)] pub struct Window { - /// The cursor of this window. - pub cursor: Cursor, + /// The cursor options of this window. Cursor icons are set with the `Cursor` component on the + /// window entity. + pub cursor_options: CursorOptions, /// What presentation mode to give the window. pub present_mode: PresentMode, /// Which fullscreen or windowing mode should be used. @@ -316,7 +315,7 @@ impl Default for Window { Self { title: "App".to_owned(), name: None, - cursor: Default::default(), + cursor_options: Default::default(), present_mode: Default::default(), mode: Default::default(), position: Default::default(), @@ -406,7 +405,7 @@ impl Window { /// /// See [`WindowResolution`] for an explanation about logical/physical sizes. #[inline] - pub fn physical_size(&self) -> bevy_math::UVec2 { + pub fn physical_size(&self) -> UVec2 { self.resolution.physical_size() } @@ -543,23 +542,20 @@ impl WindowResizeConstraints { } /// Cursor data for a [`Window`]. -#[derive(Debug, Copy, Clone, Reflect)] +#[derive(Debug, Clone, Reflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] #[reflect(Debug, Default)] -pub struct Cursor { - /// What the cursor should look like while inside the window. - pub icon: CursorIcon, - +pub struct CursorOptions { /// Whether the cursor is visible or not. /// /// ## Platform-specific /// /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. - /// To stop the cursor from leaving the window, change [`Cursor::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`] + /// To stop the cursor from leaving the window, change [`CursorOptions::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`] /// - **`macOS`**: The cursor is hidden only when the window is focused. /// - **`iOS`** and **`Android`** do not have cursors pub visible: bool, @@ -583,10 +579,9 @@ pub struct Cursor { pub hit_test: bool, } -impl Default for Cursor { +impl Default for CursorOptions { fn default() -> Self { - Cursor { - icon: CursorIcon::Default, + CursorOptions { visible: true, grab_mode: CursorGrabMode::None, hit_test: true, @@ -870,7 +865,7 @@ impl From for WindowResolution { } } -/// Defines if and how the [`Cursor`] is grabbed by a [`Window`]. +/// Defines if and how the cursor is grabbed by a [`Window`]. /// /// ## Platform-specific /// @@ -945,6 +940,8 @@ pub enum MonitorSelection { Primary, /// Uses the monitor with the specified index. Index(usize), + /// Uses a given [`crate::monitor::Monitor`] entity. + Entity(Entity), } /// Presentation mode for a [`Window`]. @@ -1092,7 +1089,7 @@ pub enum WindowMode { #[default] Windowed, /// The window should appear fullscreen by being borderless and using the full - /// size of the screen. + /// size of the screen on the given [`MonitorSelection`]. /// /// When setting this, the window's physical size will be modified to match the size /// of the current monitor resolution, and the logical size will follow based @@ -1102,8 +1099,8 @@ pub enum WindowMode { /// the window's logical size may be different from its physical size. /// If you want to avoid that behavior, you can use the [`WindowResolution::set_scale_factor_override`] function /// or the [`WindowResolution::with_scale_factor_override`] builder method to set the scale factor to 1.0. - BorderlessFullscreen, - /// The window should be in "true"/"legacy" Fullscreen mode. + BorderlessFullscreen(MonitorSelection), + /// The window should be in "true"/"legacy" Fullscreen mode on the given [`MonitorSelection`]. /// /// When setting this, the operating system will be requested to use the /// **closest** resolution available for the current monitor to match as @@ -1111,8 +1108,8 @@ pub enum WindowMode { /// After that, the window's physical size will be modified to match /// that monitor resolution, and the logical size will follow based on the /// scale factor, see [`WindowResolution`]. - SizedFullscreen, - /// The window should be in "true"/"legacy" Fullscreen mode. + SizedFullscreen(MonitorSelection), + /// The window should be in "true"/"legacy" Fullscreen mode on the given [`MonitorSelection`]. /// /// When setting this, the operating system will be requested to use the /// **biggest** resolution available for the current monitor. @@ -1124,7 +1121,7 @@ pub enum WindowMode { /// the window's logical size may be different from its physical size. /// If you want to avoid that behavior, you can use the [`WindowResolution::set_scale_factor_override`] function /// or the [`WindowResolution::with_scale_factor_override`] builder method to set the scale factor to 1.0. - Fullscreen, + Fullscreen(MonitorSelection), } /// Specifies where a [`Window`] should appear relative to other overlapping windows (on top or under) . diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 948f97ab29c6e9..2dd20a9bdd238c 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -13,7 +13,7 @@ trace = [] wayland = ["winit/wayland", "winit/wayland-csd-adwaita"] x11 = ["winit/x11"] accesskit_unix = ["accesskit_winit/accesskit_unix", "accesskit_winit/async-io"] -serialize = ["serde"] +serialize = ["serde", "bevy_input/serialize", "bevy_window/serialize"] [dependencies] # bevy @@ -52,10 +52,9 @@ wasm-bindgen = { version = "0.2" } web-sys = "0.3" crossbeam-channel = "0.5" - [lints] workspace = true [package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index 5cc2c034755286..8cb07f9600dc27 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -6,7 +6,7 @@ use bevy_input::{ ButtonState, }; use bevy_math::Vec2; -use bevy_window::{CursorIcon, EnabledButtons, WindowLevel, WindowTheme}; +use bevy_window::{EnabledButtons, SystemCursorIcon, WindowLevel, WindowTheme}; use winit::keyboard::{Key, NamedKey, NativeKey}; pub fn convert_keyboard_input( @@ -17,6 +17,7 @@ pub fn convert_keyboard_input( state: convert_element_state(keyboard_input.state), key_code: convert_physical_key_code(keyboard_input.physical_key), logical_key: convert_logical_key(&keyboard_input.logical_key), + repeat: keyboard_input.repeat, window, } } @@ -285,7 +286,7 @@ pub fn convert_physical_key_code(virtual_key_code: winit::keyboard::PhysicalKey) } } -pub fn convert_logical_key(logical_key_code: &winit::keyboard::Key) -> bevy_input::keyboard::Key { +pub fn convert_logical_key(logical_key_code: &Key) -> bevy_input::keyboard::Key { match logical_key_code { Key::Character(s) => bevy_input::keyboard::Key::Character(s.clone()), Key::Unidentified(nk) => bevy_input::keyboard::Key::Unidentified(convert_native_key(nk)), @@ -627,41 +628,42 @@ pub fn convert_native_key(native_key: &NativeKey) -> bevy_input::keyboard::Nativ } } -pub fn convert_cursor_icon(cursor_icon: CursorIcon) -> winit::window::CursorIcon { +/// Converts a [`SystemCursorIcon`] to a [`winit::window::CursorIcon`]. +pub fn convert_system_cursor_icon(cursor_icon: SystemCursorIcon) -> winit::window::CursorIcon { match cursor_icon { - CursorIcon::Crosshair => winit::window::CursorIcon::Crosshair, - CursorIcon::Pointer => winit::window::CursorIcon::Pointer, - CursorIcon::Move => winit::window::CursorIcon::Move, - CursorIcon::Text => winit::window::CursorIcon::Text, - CursorIcon::Wait => winit::window::CursorIcon::Wait, - CursorIcon::Help => winit::window::CursorIcon::Help, - CursorIcon::Progress => winit::window::CursorIcon::Progress, - CursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed, - CursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu, - CursorIcon::Cell => winit::window::CursorIcon::Cell, - CursorIcon::VerticalText => winit::window::CursorIcon::VerticalText, - CursorIcon::Alias => winit::window::CursorIcon::Alias, - CursorIcon::Copy => winit::window::CursorIcon::Copy, - CursorIcon::NoDrop => winit::window::CursorIcon::NoDrop, - CursorIcon::Grab => winit::window::CursorIcon::Grab, - CursorIcon::Grabbing => winit::window::CursorIcon::Grabbing, - CursorIcon::AllScroll => winit::window::CursorIcon::AllScroll, - CursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn, - CursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut, - CursorIcon::EResize => winit::window::CursorIcon::EResize, - CursorIcon::NResize => winit::window::CursorIcon::NResize, - CursorIcon::NeResize => winit::window::CursorIcon::NeResize, - CursorIcon::NwResize => winit::window::CursorIcon::NwResize, - CursorIcon::SResize => winit::window::CursorIcon::SResize, - CursorIcon::SeResize => winit::window::CursorIcon::SeResize, - CursorIcon::SwResize => winit::window::CursorIcon::SwResize, - CursorIcon::WResize => winit::window::CursorIcon::WResize, - CursorIcon::EwResize => winit::window::CursorIcon::EwResize, - CursorIcon::NsResize => winit::window::CursorIcon::NsResize, - CursorIcon::NeswResize => winit::window::CursorIcon::NeswResize, - CursorIcon::NwseResize => winit::window::CursorIcon::NwseResize, - CursorIcon::ColResize => winit::window::CursorIcon::ColResize, - CursorIcon::RowResize => winit::window::CursorIcon::RowResize, + SystemCursorIcon::Crosshair => winit::window::CursorIcon::Crosshair, + SystemCursorIcon::Pointer => winit::window::CursorIcon::Pointer, + SystemCursorIcon::Move => winit::window::CursorIcon::Move, + SystemCursorIcon::Text => winit::window::CursorIcon::Text, + SystemCursorIcon::Wait => winit::window::CursorIcon::Wait, + SystemCursorIcon::Help => winit::window::CursorIcon::Help, + SystemCursorIcon::Progress => winit::window::CursorIcon::Progress, + SystemCursorIcon::NotAllowed => winit::window::CursorIcon::NotAllowed, + SystemCursorIcon::ContextMenu => winit::window::CursorIcon::ContextMenu, + SystemCursorIcon::Cell => winit::window::CursorIcon::Cell, + SystemCursorIcon::VerticalText => winit::window::CursorIcon::VerticalText, + SystemCursorIcon::Alias => winit::window::CursorIcon::Alias, + SystemCursorIcon::Copy => winit::window::CursorIcon::Copy, + SystemCursorIcon::NoDrop => winit::window::CursorIcon::NoDrop, + SystemCursorIcon::Grab => winit::window::CursorIcon::Grab, + SystemCursorIcon::Grabbing => winit::window::CursorIcon::Grabbing, + SystemCursorIcon::AllScroll => winit::window::CursorIcon::AllScroll, + SystemCursorIcon::ZoomIn => winit::window::CursorIcon::ZoomIn, + SystemCursorIcon::ZoomOut => winit::window::CursorIcon::ZoomOut, + SystemCursorIcon::EResize => winit::window::CursorIcon::EResize, + SystemCursorIcon::NResize => winit::window::CursorIcon::NResize, + SystemCursorIcon::NeResize => winit::window::CursorIcon::NeResize, + SystemCursorIcon::NwResize => winit::window::CursorIcon::NwResize, + SystemCursorIcon::SResize => winit::window::CursorIcon::SResize, + SystemCursorIcon::SeResize => winit::window::CursorIcon::SeResize, + SystemCursorIcon::SwResize => winit::window::CursorIcon::SwResize, + SystemCursorIcon::WResize => winit::window::CursorIcon::WResize, + SystemCursorIcon::EwResize => winit::window::CursorIcon::EwResize, + SystemCursorIcon::NsResize => winit::window::CursorIcon::NsResize, + SystemCursorIcon::NeswResize => winit::window::CursorIcon::NeswResize, + SystemCursorIcon::NwseResize => winit::window::CursorIcon::NwseResize, + SystemCursorIcon::ColResize => winit::window::CursorIcon::ColResize, + SystemCursorIcon::RowResize => winit::window::CursorIcon::RowResize, _ => winit::window::CursorIcon::Default, } } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 82219b134cadf8..4901790ac7915b 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -12,6 +12,7 @@ //! The app's [runner](bevy_app::App::runner) is set by `WinitPlugin` and handles the `winit` [`EventLoop`]. //! See `winit_runner` for details. +use bevy_derive::Deref; use bevy_window::RawHandleWrapperHolder; use std::marker::PhantomData; use winit::event_loop::EventLoop; @@ -23,14 +24,21 @@ use bevy_app::{App, Last, Plugin}; use bevy_ecs::prelude::*; #[allow(deprecated)] use bevy_window::{exit_on_all_closed, Window, WindowCreated}; -pub use system::create_windows; +pub use converters::convert_system_cursor_icon; +pub use state::{CursorSource, CustomCursorCache, CustomCursorCacheKey, PendingCursor}; use system::{changed_windows, despawn_windows}; +pub use system::{create_monitors, create_windows}; +pub use winit::event_loop::EventLoopProxy; +#[cfg(all(target_family = "wasm", target_os = "unknown"))] +pub use winit::platform::web::CustomCursorExtWebSys; +pub use winit::window::{CustomCursor as WinitCustomCursor, CustomCursorSource}; pub use winit_config::*; pub use winit_event::*; pub use winit_windows::*; use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers}; use crate::state::winit_runner; +use crate::winit_monitors::WinitMonitors; pub mod accessibility; mod converters; @@ -38,6 +46,7 @@ mod state; mod system; mod winit_config; pub mod winit_event; +mod winit_monitors; mod winit_windows; /// [`AndroidApp`] provides an interface to query the application state as well as monitor events @@ -111,6 +120,7 @@ impl Plugin for WinitPlugin { } app.init_non_send_resource::() + .init_resource::() .init_resource::() .add_event::() .set_runner(winit_runner::) @@ -142,12 +152,14 @@ impl Plugin for WinitPlugin { #[derive(Debug, Default, Clone, Copy, Event)] pub struct WakeUp; -/// A re-export of [`winit::event_loop::EventLoopProxy`]. +/// A wrapper type around [`winit::event_loop::EventLoopProxy`] with the specific +/// [`winit::event::Event::UserEvent`] used in the [`WinitPlugin`]. /// /// The `EventLoopProxy` can be used to request a redraw from outside bevy. /// -/// Use `NonSend` to receive this resource. -pub type EventLoopProxy = winit::event_loop::EventLoopProxy; +/// Use `Res` to receive this resource. +#[derive(Resource, Deref)] +pub struct EventLoopProxyWrapper(EventLoopProxy); trait AppSendEvent { fn send(&mut self, event: impl Into); @@ -177,4 +189,8 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( NonSendMut<'w, AccessKitAdapters>, ResMut<'w, WinitActionRequestHandlers>, Res<'w, AccessibilityRequested>, + Res<'w, WinitMonitors>, ); + +/// The parameters of the [`create_monitors`] system. +pub type CreateMonitorParams<'w, 's> = (Commands<'w, 's>, ResMut<'w, WinitMonitors>); diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index c7c8bcc572ad5c..871a559b060a81 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -15,7 +15,7 @@ use bevy_log::{error, trace, warn}; use bevy_math::{ivec2, DVec2, Vec2}; #[cfg(not(target_arch = "wasm32"))] use bevy_tasks::tick_global_task_pools_on_main_thread; -use bevy_utils::Instant; +use bevy_utils::{HashMap, Instant}; use std::marker::PhantomData; use winit::application::ApplicationHandler; use winit::dpi::PhysicalSize; @@ -35,10 +35,10 @@ use bevy_window::{ use bevy_window::{PrimaryWindow, RawHandleWrapper}; use crate::accessibility::AccessKitAdapters; -use crate::system::CachedWindow; +use crate::system::{create_monitors, CachedWindow}; use crate::{ - converters, create_windows, AppSendEvent, CreateWindowParams, UpdateMode, WinitEvent, - WinitSettings, WinitWindows, + converters, create_windows, AppSendEvent, CreateMonitorParams, CreateWindowParams, + EventLoopProxyWrapper, UpdateMode, WinitEvent, WinitSettings, WinitWindows, }; /// Persistent state that is used to run the [`App`] according to the current @@ -85,7 +85,7 @@ struct WinitAppRunnerState { impl WinitAppRunnerState { fn new(mut app: App) -> Self { - app.add_event::(); + app.add_event::().init_resource::(); let event_writer_system_state: SystemState<( EventWriter, @@ -131,6 +131,39 @@ impl WinitAppRunnerState { } } +/// Identifiers for custom cursors used in caching. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum CustomCursorCacheKey { + /// u64 is used instead of `AssetId`, because `bevy_asset` can't be imported here. + AssetIndex(u64), + /// u128 is used instead of `AssetId`, because `bevy_asset` can't be imported here. + AssetUuid(u128), + /// A URL to a cursor. + Url(String), +} + +/// Caches custom cursors. On many platforms, creating custom cursors is expensive, especially on +/// the web. +#[derive(Debug, Clone, Default, Resource)] +pub struct CustomCursorCache(pub HashMap); + +/// A source for a cursor. Is created in `bevy_render` and consumed by the winit event loop. +#[derive(Debug)] +pub enum CursorSource { + /// A custom cursor was identified to be cached, no reason to recreate it. + CustomCached(CustomCursorCacheKey), + /// A custom cursor was not cached, so it needs to be created by the winit event loop. + Custom((CustomCursorCacheKey, winit::window::CustomCursorSource)), + /// A system cursor was requested. + System(winit::window::CursorIcon), +} + +/// Component that indicates what cursor should be used for a window. Inserted +/// automatically after changing `CursorIcon` and consumed by the winit event +/// loop. +#[derive(Component, Debug)] +pub struct PendingCursor(pub Option); + impl ApplicationHandler for WinitAppRunnerState { fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { if event_loop.exiting() { @@ -401,10 +434,13 @@ impl ApplicationHandler for WinitAppRunnerState { } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let mut create_monitor = SystemState::::from_world(self.world_mut()); // create any new windows // (even if app did not update, some may have been created by plugin setup) let mut create_window = SystemState::>>::from_world(self.world_mut()); + create_monitors(event_loop, create_monitor.get_mut(self.world_mut())); + create_monitor.apply(self.world_mut()); create_windows(event_loop, create_window.get_mut(self.world_mut())); create_window.apply(self.world_mut()); @@ -475,6 +511,7 @@ impl ApplicationHandler for WinitAppRunnerState { mut adapters, mut handlers, accessibility_requested, + monitors, ) = create_window.get_mut(self.world_mut()); let winit_window = winit_windows.create_window( @@ -484,6 +521,7 @@ impl ApplicationHandler for WinitAppRunnerState { &mut adapters, &mut handlers, &accessibility_requested, + &monitors, ); let wrapper = RawHandleWrapper::new(winit_window).unwrap(); @@ -504,9 +542,18 @@ impl ApplicationHandler for WinitAppRunnerState { let begin_frame_time = Instant::now(); if should_update { + let (_, windows) = focused_windows_state.get(self.world()); + // If no windows exist, this will evaluate to `true`. + let all_invisible = windows.iter().all(|w| !w.1.visible); + // Not redrawing, but the timeout elapsed. - if !self.ran_update_since_last_redraw { + // + // Additional condition for Windows OS. + // If no windows are visible, redraw calls will never succeed, which results in no app update calls being performed. + // This is a temporary solution, full solution is mentioned here: https://github.com/bevyengine/bevy/issues/1343#issuecomment-770091684 + if !self.ran_update_since_last_redraw || all_invisible { self.run_app_update(); + self.update_cursors(event_loop); self.ran_update_since_last_redraw = true; } else { self.redraw_requested = true; @@ -515,7 +562,6 @@ impl ApplicationHandler for WinitAppRunnerState { // Running the app may have changed the WinitSettings resource, so we have to re-extract it. let (config, windows) = focused_windows_state.get(self.world()); let focused = windows.iter().any(|(_, window)| window.focused); - update_mode = config.update_mode(focused); } @@ -737,6 +783,42 @@ impl WinitAppRunnerState { .resource_mut::>() .send_batch(buffered_events); } + + fn update_cursors(&mut self, event_loop: &ActiveEventLoop) { + let mut windows_state: SystemState<( + NonSendMut, + ResMut, + Query<(Entity, &mut PendingCursor), Changed>, + )> = SystemState::new(self.world_mut()); + let (winit_windows, mut cursor_cache, mut windows) = + windows_state.get_mut(self.world_mut()); + + for (entity, mut pending_cursor) in windows.iter_mut() { + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + let Some(pending_cursor) = pending_cursor.0.take() else { + continue; + }; + + let final_cursor: winit::window::Cursor = match pending_cursor { + CursorSource::CustomCached(cache_key) => { + let Some(cached_cursor) = cursor_cache.0.get(&cache_key) else { + error!("Cursor should have been cached, but was not found"); + continue; + }; + cached_cursor.clone().into() + } + CursorSource::Custom((cache_key, cursor)) => { + let custom_cursor = event_loop.create_custom_cursor(cursor); + cursor_cache.0.insert(cache_key, custom_cursor.clone()); + custom_cursor.into() + } + CursorSource::System(system_cursor) => system_cursor.into(), + }; + winit_window.set_cursor(final_cursor); + } + } } /// The default [`App::runner`] for the [`WinitPlugin`](crate::WinitPlugin) plugin. @@ -755,12 +837,12 @@ pub fn winit_runner(mut app: App) -> AppExit { .unwrap(); app.world_mut() - .insert_non_send_resource(event_loop.create_proxy()); + .insert_resource(EventLoopProxyWrapper(event_loop.create_proxy())); let mut runner_state = WinitAppRunnerState::new(app); trace!("starting winit event loop"); - // TODO(clean): the winit docs mention using `spawn` instead of `run` on WASM. + // TODO(clean): the winit docs mention using `spawn` instead of `run` on Wasm. if let Err(err) = event_loop.run_app(&mut runner_state) { error!("winit event loop returned an error: {err}"); } diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 7a0dd51a9a8619..86d3b24acba479 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -8,8 +8,8 @@ use bevy_ecs::{ }; use bevy_utils::tracing::{error, info, warn}; use bevy_window::{ - ClosingWindow, RawHandleWrapper, Window, WindowClosed, WindowClosing, WindowCreated, - WindowMode, WindowResized, WindowWrapper, + ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, + WindowClosing, WindowCreated, WindowMode, WindowResized, WindowWrapper, }; use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; @@ -18,18 +18,21 @@ use winit::event_loop::ActiveEventLoop; use bevy_app::AppExit; use bevy_ecs::prelude::EventReader; use bevy_ecs::query::With; +use bevy_ecs::system::Res; +use bevy_math::{IVec2, UVec2}; #[cfg(target_os = "ios")] use winit::platform::ios::WindowExtIOS; #[cfg(target_arch = "wasm32")] use winit::platform::web::WindowExtWebSys; use crate::state::react_to_resize; +use crate::winit_monitors::WinitMonitors; use crate::{ converters::{ - self, convert_enabled_buttons, convert_window_level, convert_window_theme, - convert_winit_theme, + convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme, }, - get_best_videomode, get_fitting_videomode, CreateWindowParams, WinitWindows, + get_best_videomode, get_fitting_videomode, select_monitor, CreateMonitorParams, + CreateWindowParams, WinitWindows, }; /// Creates new windows on the [`winit`] backend for each entity with a newly-added @@ -48,6 +51,7 @@ pub fn create_windows( mut adapters, mut handlers, accessibility_requested, + monitors, ): SystemParamItem>, ) { for (entity, mut window, handle_holder) in &mut created_windows { @@ -68,6 +72,7 @@ pub fn create_windows( &mut adapters, &mut handlers, &accessibility_requested, + &monitors, ); if let Some(theme) = winit_window.theme() { @@ -118,6 +123,69 @@ pub fn create_windows( } } +/// Synchronize available monitors as reported by [`winit`] with [`Monitor`] entities in the world. +pub fn create_monitors( + event_loop: &ActiveEventLoop, + (mut commands, mut monitors): SystemParamItem, +) { + let primary_monitor = event_loop.primary_monitor(); + let mut seen_monitors = vec![false; monitors.monitors.len()]; + + 'outer: for monitor in event_loop.available_monitors() { + for (idx, (m, _)) in monitors.monitors.iter().enumerate() { + if &monitor == m { + seen_monitors[idx] = true; + continue 'outer; + } + } + + let size = monitor.size(); + let position = monitor.position(); + + let entity = commands + .spawn(Monitor { + name: monitor.name(), + physical_height: size.height, + physical_width: size.width, + physical_position: IVec2::new(position.x, position.y), + refresh_rate_millihertz: monitor.refresh_rate_millihertz(), + scale_factor: monitor.scale_factor(), + video_modes: monitor + .video_modes() + .map(|v| { + let size = v.size(); + VideoMode { + physical_size: UVec2::new(size.width, size.height), + bit_depth: v.bit_depth(), + refresh_rate_millihertz: v.refresh_rate_millihertz(), + } + }) + .collect(), + }) + .id(); + + if primary_monitor.as_ref() == Some(&monitor) { + commands.entity(entity).insert(PrimaryMonitor); + } + + seen_monitors.push(true); + monitors.monitors.push((monitor, entity)); + } + + let mut idx = 0; + monitors.monitors.retain(|(_m, entity)| { + if seen_monitors[idx] { + idx += 1; + true + } else { + info!("Monitor removed {:?}", entity); + commands.entity(*entity).despawn(); + idx += 1; + false + } + }); +} + #[allow(clippy::too_many_arguments)] pub(crate) fn despawn_windows( closing: Query>, @@ -178,6 +246,7 @@ pub struct CachedWindow { pub(crate) fn changed_windows( mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed>, winit_windows: NonSendMut, + monitors: Res, mut window_resized: EventWriter, ) { for (entity, mut window, mut cache) in &mut changed_windows { @@ -191,26 +260,44 @@ pub(crate) fn changed_windows( if window.mode != cache.window.mode { let new_mode = match window.mode { - WindowMode::BorderlessFullscreen => { - Some(Some(winit::window::Fullscreen::Borderless(None))) + WindowMode::BorderlessFullscreen(monitor_selection) => { + Some(Some(winit::window::Fullscreen::Borderless(select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + )))) } - mode @ (WindowMode::Fullscreen | WindowMode::SizedFullscreen) => { - if let Some(current_monitor) = winit_window.current_monitor() { - let videomode = match mode { - WindowMode::Fullscreen => get_best_videomode(¤t_monitor), - WindowMode::SizedFullscreen => get_fitting_videomode( - ¤t_monitor, - window.width() as u32, - window.height() as u32, - ), - _ => unreachable!(), - }; - - Some(Some(winit::window::Fullscreen::Exclusive(videomode))) - } else { - warn!("Could not determine current monitor, ignoring exclusive fullscreen request for window {:?}", window.title); - None - } + mode @ (WindowMode::Fullscreen(_) | WindowMode::SizedFullscreen(_)) => { + let videomode = match mode { + WindowMode::Fullscreen(monitor_selection) => get_best_videomode( + &select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + ), + WindowMode::SizedFullscreen(monitor_selection) => get_fitting_videomode( + &select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + window.width() as u32, + window.height() as u32, + ), + _ => unreachable!(), + }; + + Some(Some(winit::window::Fullscreen::Exclusive(videomode))) } WindowMode::Windowed => Some(None), }; @@ -277,21 +364,17 @@ pub(crate) fn changed_windows( } } - if window.cursor.icon != cache.window.cursor.icon { - winit_window.set_cursor(converters::convert_cursor_icon(window.cursor.icon)); - } - - if window.cursor.grab_mode != cache.window.cursor.grab_mode { - crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode); + if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode { + crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode); } - if window.cursor.visible != cache.window.cursor.visible { - winit_window.set_cursor_visible(window.cursor.visible); + if window.cursor_options.visible != cache.window.cursor_options.visible { + winit_window.set_cursor_visible(window.cursor_options.visible); } - if window.cursor.hit_test != cache.window.cursor.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { - window.cursor.hit_test = cache.window.cursor.hit_test; + if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { + window.cursor_options.hit_test = cache.window.cursor_options.hit_test; warn!( "Could not set cursor hit test for window {:?}: {:?}", window.title, err @@ -336,7 +419,7 @@ pub(crate) fn changed_windows( if let Some(position) = crate::winit_window_position( &window.position, &window.resolution, - winit_window.available_monitors(), + &monitors, winit_window.primary_monitor(), winit_window.current_monitor(), ) { diff --git a/crates/bevy_winit/src/winit_monitors.rs b/crates/bevy_winit/src/winit_monitors.rs new file mode 100644 index 00000000000000..0b8c0073e8a0e8 --- /dev/null +++ b/crates/bevy_winit/src/winit_monitors.rs @@ -0,0 +1,35 @@ +use winit::monitor::MonitorHandle; + +use bevy_ecs::entity::Entity; +use bevy_ecs::system::Resource; + +/// Stores [`winit`] monitors and their corresponding entities +/// +/// # Known Issues +/// +/// On some platforms, physically disconnecting a monitor might result in a +/// panic in [`winit`]'s loop. This will lead to a crash in the bevy app. See +/// [13669] for investigations and discussions. +/// +/// [13669]: https://github.com/bevyengine/bevy/pull/13669 +#[derive(Resource, Debug, Default)] +pub struct WinitMonitors { + /// Stores [`winit`] monitors and their corresponding entities + // We can't use a `BtreeMap` here because clippy complains about using `MonitorHandle` as a key + // on some platforms. Using a `Vec` is fine because we don't expect to have a large number of + // monitors and avoids having to audit the code for `MonitorHandle` equality. + pub(crate) monitors: Vec<(MonitorHandle, Entity)>, +} + +impl WinitMonitors { + pub fn nth(&self, n: usize) -> Option { + self.monitors.get(n).map(|(monitor, _)| monitor.clone()) + } + + pub fn find_entity(&self, entity: Entity) -> Option { + self.monitors + .iter() + .find(|(_, e)| *e == entity) + .map(|(monitor, _)| monitor.clone()) + } +} diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 0fc30e3b3e1f2d..fd1811f1ce3cb2 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -4,7 +4,8 @@ use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; use bevy_utils::{tracing::warn, HashMap}; use bevy_window::{ - CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution, WindowWrapper, + CursorGrabMode, MonitorSelection, Window, WindowMode, WindowPosition, WindowResolution, + WindowWrapper, }; use winit::{ @@ -14,6 +15,7 @@ use winit::{ window::{CursorGrabMode as WinitCursorGrabMode, Fullscreen, Window as WinitWindow, WindowId}, }; +use crate::winit_monitors::WinitMonitors; use crate::{ accessibility::{ prepare_accessibility_for_window, AccessKitAdapters, WinitActionRequestHandlers, @@ -39,6 +41,7 @@ pub struct WinitWindows { impl WinitWindows { /// Creates a `winit` window and associates it with our entity. + #[allow(clippy::too_many_arguments)] pub fn create_window( &mut self, event_loop: &ActiveEventLoop, @@ -47,6 +50,7 @@ impl WinitWindows { adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, + monitors: &WinitMonitors, ) -> &WindowWrapper { let mut winit_window_attributes = WinitWindow::default_attributes(); @@ -55,31 +59,49 @@ impl WinitWindows { winit_window_attributes = winit_window_attributes.with_visible(false); winit_window_attributes = match window.mode { - WindowMode::BorderlessFullscreen => winit_window_attributes - .with_fullscreen(Some(Fullscreen::Borderless(event_loop.primary_monitor()))), - mode @ (WindowMode::Fullscreen | WindowMode::SizedFullscreen) => { - if let Some(primary_monitor) = event_loop.primary_monitor() { - let videomode = match mode { - WindowMode::Fullscreen => get_best_videomode(&primary_monitor), - WindowMode::SizedFullscreen => get_fitting_videomode( - &primary_monitor, - window.width() as u32, - window.height() as u32, - ), - _ => unreachable!(), - }; - - winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode))) - } else { - warn!("Could not determine primary monitor, ignoring exclusive fullscreen request for window {:?}", window.title); - winit_window_attributes - } + WindowMode::BorderlessFullscreen(monitor_selection) => winit_window_attributes + .with_fullscreen(Some(Fullscreen::Borderless(select_monitor( + monitors, + event_loop.primary_monitor(), + None, + &monitor_selection, + )))), + mode @ (WindowMode::Fullscreen(_) | WindowMode::SizedFullscreen(_)) => { + let videomode = match mode { + WindowMode::Fullscreen(monitor_selection) => get_best_videomode( + &select_monitor( + monitors, + event_loop.primary_monitor(), + None, + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + ), + WindowMode::SizedFullscreen(monitor_selection) => get_fitting_videomode( + &select_monitor( + monitors, + event_loop.primary_monitor(), + None, + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + window.width() as u32, + window.height() as u32, + ), + _ => unreachable!(), + }; + + winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode))) } WindowMode::Windowed => { if let Some(position) = winit_window_position( &window.position, &window.resolution, - event_loop.available_monitors(), + monitors, event_loop.primary_monitor(), None, ) { @@ -225,16 +247,16 @@ impl WinitWindows { ); // Do not set the grab mode on window creation if it's none. It can fail on mobile. - if window.cursor.grab_mode != CursorGrabMode::None { - attempt_grab(&winit_window, window.cursor.grab_mode); + if window.cursor_options.grab_mode != CursorGrabMode::None { + attempt_grab(&winit_window, window.cursor_options.grab_mode); } - winit_window.set_cursor_visible(window.cursor.visible); + winit_window.set_cursor_visible(window.cursor_options.visible); // Do not set the cursor hittest on window creation if it's false, as it will always fail on // some platforms and log an unfixable warning. - if !window.cursor.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { + if !window.cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { warn!( "Could not set cursor hit test for window {:?}: {:?}", window.title, err @@ -354,7 +376,7 @@ pub(crate) fn attempt_grab(winit_window: &WinitWindow, grab_mode: CursorGrabMode pub fn winit_window_position( position: &WindowPosition, resolution: &WindowResolution, - mut available_monitors: impl Iterator, + monitors: &WinitMonitors, primary_monitor: Option, current_monitor: Option, ) -> Option> { @@ -364,17 +386,12 @@ pub fn winit_window_position( None } WindowPosition::Centered(monitor_selection) => { - use bevy_window::MonitorSelection::*; - let maybe_monitor = match monitor_selection { - Current => { - if current_monitor.is_none() { - warn!("Can't select current monitor on window creation or cannot find current monitor!"); - } - current_monitor - } - Primary => primary_monitor, - Index(n) => available_monitors.nth(*n), - }; + let maybe_monitor = select_monitor( + monitors, + primary_monitor, + current_monitor, + monitor_selection, + ); if let Some(monitor) = maybe_monitor { let screen_size = monitor.size(); @@ -410,3 +427,25 @@ pub fn winit_window_position( } } } + +/// Selects a monitor based on the given [`MonitorSelection`]. +pub fn select_monitor( + monitors: &WinitMonitors, + primary_monitor: Option, + current_monitor: Option, + monitor_selection: &MonitorSelection, +) -> Option { + use bevy_window::MonitorSelection::*; + + match monitor_selection { + Current => { + if current_monitor.is_none() { + warn!("Can't select current monitor on window creation or cannot find current monitor!"); + } + current_monitor + } + Primary => primary_monitor, + Index(n) => monitors.nth(*n), + Entity(entity) => monitors.find_entity(*entity), + } +} diff --git a/docs-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index f173c67445b6d2..fd3f7e27c1c4a9 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -49,7 +49,7 @@ git checkout v0.4.0 - [iOS](#ios) - [Setup](#setup-1) - [Build & Run](#build--run-1) - - [WASM](#wasm) + - [Wasm](#wasm) - [Setup](#setup-2) - [Build & Run](#build--run-2) - [WebGL2 and WebGPU](#webgl2-and-webgpu) @@ -195,7 +195,7 @@ Example | File | Description --- | --- | --- `ios` | [`mobile/src/lib.rs`](./mobile/src/lib.rs) | A 3d Scene with a button and playing sound -## WASM +## Wasm ### Setup diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 09873c89ee4505..d2cbf06a23b642 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -54,7 +54,6 @@ The default feature set enables most of the expected features of a game engine, |bevy_ci_testing|Enable systems that allow for automated testing on CI| |bevy_debug_stepping|Enable stepping-based debugging of Bevy systems| |bevy_dev_tools|Provides a collection of developer tools| -|bevy_dynamic_plugin|Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading))| |bmp|BMP image format support| |dds|DDS compressed texture support| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| @@ -75,6 +74,7 @@ The default feature set enables most of the expected features of a game engine, |pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pnm|PNM image format support, includes pam, pbm, pgm and ppm| +|reflect_functions|Enable function reflection| |serialize|Enable serialization support through serde| |shader_format_glsl|Enable support for shaders in GLSL| |shader_format_spirv|Enable support for shaders in SPIR-V| @@ -89,6 +89,7 @@ The default feature set enables most of the expected features of a game engine, |trace_chrome|Tracing support, saving a file in Chrome Tracing format| |trace_tracy|Tracing support, exposing a port for Tracy| |trace_tracy_memory|Tracing support, with memory profiling, exposing a port for Tracy| +|track_change_detection|Enables source location tracking for change detection, which can assist with debugging| |wav|WAV audio format support| |wayland|Wayland display server support| |webgpu|Enable support for WebGPU in Wasm. When enabled, this feature will override the `webgl2` feature and you won't be able to run Wasm builds with WebGL2, only with WebGPU.| diff --git a/docs/release_checklist.md b/docs/release_checklist.md deleted file mode 100644 index b0c005ebb43028..00000000000000 --- a/docs/release_checklist.md +++ /dev/null @@ -1,89 +0,0 @@ -# Release Checklist - -## Minor Version - -### Minor Pre-release - -1. Check regressions tag. -2. Check appropriate milestone, and close it. -3. Check GitHub Projects page for staleness. -4. Update change log. -5. Create migration guide. -6. Write blog post. -7. Update book. -8. Bump version number for all crates, using the "Release" workflow. - * Change the commit message to be nicer -9. Create tag on GitHub. -10. Edit Github Release. Add links to the `Release announcement` and `Migration Guide`. -11. Bump `latest` tag to most recent release. -12. Run this workflow to update screenshots: - * - * _This will block blog post releases (and take ~40 minutes) so do it early_. -13. Run this workflow to update wasm examples: - * - -### Minor Release - -1. Release on crates.io - * `bash tools/publish.sh` -2. Announce on: - 1. HackerNews - 2. Twitter - 3. Reddit: /r/bevy, /r/rust, /r/rust_gamedev - 4. Discord: Bevy, Game Development in Rust, Rust Programming Language Community - 5. This Month in Rust Game Development newsletter - 6. This Week in Rust newsletter - -### Minor Post-release - -1. Bump version number for all crates to next versions, as `0.X-dev`, using the "Post-release version bump" workflow, to ensure properly displayed version for [Dev Docs](https://dev-docs.bevyengine.org/bevy/index.html). -2. Update Bevy version used for Bevy book code validation to latest release. - -## Patch - -### Patch Pre-release - -1. Check appropriate milestone. -2. Close the milestone, open the next one if anything remains and transfer them. -3. Bump version number for all crates, using the command from the "Release" workflow locally, with `patch` for the new version. At the time of writing this: - * `cargo release patch --workspace --no-publish --execute --no-tag --no-confirm --no-push --dependent-version upgrade --exclude ci --exclude errors --exclude bevy_mobile_example --exclude build-wasm-example` - * Change the commit message to be nicer -4. Create tag on GitHub. -5. Edit Github Release. Add link to the comparison between this patch and the previous version. -6. Bump `latest` tag to most recent release. -7. Run this workflow to update screenshots: - * -8. Run this workflow to update wasm examples: - * - -### Patch Release - -1. Release on crates.io - * `bash tools/publish.sh` -2. Announce on: - 1. Discord: Bevy - -### Patch Post-Release - -## Release Candidate - -### RC Pre-Release - -1. Check appropriate milestone. -2. Create a branch for the release. -3. Bump version number for all crates, using the command from the "Release" workflow locally, with `rc` for the new version. At the time of writing this: - * `cargo release rc --workspace --no-publish --execute --no-tag --no-confirm --no-push --dependent-version upgrade --exclude ci --exclude errors --exclude bevy_mobile_example --exclude build-wasm-example` - * Change the commit message to be nicer -4. Create tag on GitHub. -5. Edit Github Release. Add link to the comparison between this rc and the previous version. - -### RC Release - -1. Release on crates.io - * `bash tools/publish.sh` -2. Announce on: - 1. Discord: Bevy, #dev-announcements - -### RC Post-Release - -1. Update Bevy version used for Bevy book code validation to latest release. diff --git a/docs/the_bevy_organization.md b/docs/the_bevy_organization.md deleted file mode 100644 index 22b974ad08a1ba..00000000000000 --- a/docs/the_bevy_organization.md +++ /dev/null @@ -1,72 +0,0 @@ -# The Bevy Organization - -The Bevy Organization is the group of people responsible for stewarding the Bevy project. It handles things like merging pull requests, choosing project direction, managing bugs / issues / feature requests, running the Bevy website, controlling access to secrets, defining and enforcing best practices, etc. - -Note that you _do not_ need to be a member of the Bevy Organization to contribute to Bevy. Community contributors (this means you) can freely open issues, submit pull requests, and review pull requests. - -The Bevy Organization is currently broken up into the following roles: - -## Project Lead - -Project Leads have the final call on all design and code changes within Bevy. This is to ensure a coherent vision and consistent quality of code. They are responsible for representing the project publicly and interacting with other entities (companies, organizations, etc) on behalf of the project. They choose how the project is organized, which includes how responsibility is delegated. Project Leads implicitly have the power of other roles (Maintainer, Subject Matter Expert, etc). - -@cart is, for now, our singular project lead. @cart tries to be accountable: open to new ideas and to changing his mind in the face of compelling arguments or community consensus. - -## Maintainer - -Maintainers have merge rights in Bevy repos. They assess the scope of pull requests and whether they fit into the Bevy project's vision. They also serve as representatives of the Bevy project and are often the interface between the Bevy community and the Bevy project. They assist the Project Leads in moderating the community, handling administrative tasks, defining best practices, choosing project direction, and deciding how the project is organized. - -Maintainers abide by the following rules when merging pull requests: - -1. Trivial PRs can be merged without approvals. -2. Relatively uncontroversial PRs can be merged following approval from at least two community members (including Maintainers) with appropriate expertise. -3. Controversial PRs cannot be merged unless they have the approval of a Project Lead or two Subject Matter Experts (in the "area" of the PR). -4. If two Maintainers have approved a controversial PR they can "start the clock" on a PR by adding it to [this queue](https://github.com/orgs/bevyengine/projects/6). If 45 days elapse without SME or Project Lead action (approval, feedback or an explicit request to defer), the PR can be merged by maintainers. - -We choose new Maintainers carefully and only after they have proven themselves in the Bevy community. Maintainers must have a proven track record of the following: - -1. **A strong understanding of the Bevy project as a whole**: our vision, our development process, and our community -2. **Solid technical skills and code contributions across most engine areas**: Maintainers must be able to evaluate the scope of pull requests, provide complete code reviews, ensure the appropriate people have signed off on a PR, and decide if changes align with our vision for Bevy. This can only be done if Maintainers are technical experts, both generically across engine subject areas, and more specifically in the Bevy codebase. -3. **Great social skills**: Maintainers regularly deal with and resolve "community issues". They must always present a professional and friendly face. They are representatives of the project and their actions directly reflect our goals and values. Working with them should not be painful. -4. **Thorough reviews of other peoples' PRs**: Maintainers are the last line of defense when protecting project vision and code quality. They are also often the first people new contributors interact with. They must have a history of leaving thorough and helpful code reviews. -5. **Ethical and trustworthy behavior**: Maintainers are granted significant administrative permissions. They must be trustable. - -To make it easy to reach consensus, hold a high quality bar, and synchronize vision, we intentionally keep the Maintainer team small. We choose new maintainers carefully and only after they have proven themselves in the Bevy community. - -If you are interested in a Maintainer role and believe you meet these criteria, reach out to one of our Project Leads or Maintainers. One month after every Bevy release Maintainers and Project Leads will evaluate the need for new roles, review candidates, and vote. Bringing in a new Maintainer requires unanimous support from all Project Leads and Maintainers. - -Check out the [Bevy People](https://bevyengine.org/community/people/#the-bevy-organization) page for the current list of maintainers. - -## Subject Matter Expert (SME) - -Subject Matter Experts are members of the Bevy Organization that have proven themselves to be experts in a given development area (Rendering, Assets, ECS, UI, etc) and have a solid understanding of the Bevy Organization's vision for that area. They are great people to reach out to if you have questions about a given area of Bevy. - -SME approvals count as "votes" on controversial PRs (provided the PR is in their "subject area"). This includes [RFCs](https://github.com/bevyengine/rfcs). If a controversial PR has two votes from Subject Matter Experts in that PR's area, it can be merged without Project Lead approval. If a SME creates a PR in their subject area, this does count as a vote. However, Project Leads have the right to revert changes merged this way, so it is each SME's responsibility to ensure they have synced up with the Project Lead's vision. Additionally, when approving a design, consensus between SMEs and Project Leads (and ideally most of the wider Bevy community) is heavily encouraged. Merging without consensus risks fractured project vision and/or ping-ponging between designs. The "larger" the impact of a design, the more critical it is to establish consensus. - -We choose new SMEs carefully and only after they have proven themselves in the Bevy community. SMEs must have a proven track record of the following: - -1. **Designing and contributing to foundational pieces in their subject area**: SMEs are responsible for building and extending the foundations of a given subject area. They must have a history of doing this before becoming an SME. -2. **Thorough reviews of other peoples' PRs in their subject area**: Within a subject area, SMEs are responsible for guiding people in the correct technical direction and only approving things aligned with that vision. They must have a history of doing this before becoming an SME. -3. **Great social skills**: Within a subject area, SMEs are responsible for reviewing peoples' code, communicating project vision, and establishing consensus. They are representatives of the project and their actions directly reflect our goals and values. Working with them should not be painful. - -To make it easy to reach consensus, hold a high quality bar, and synchronize vision, we intentionally keep the number of SMEs in a given area small: 2 is the absolute minimum (to allow voting to occur), 3 is preferred, and 4 will be allowed in some cases. Bevy Organization members can be SMEs in more than one area, and Maintainers can also be SMEs. - -If you are interested in a SME role and believe you meet these criteria, reach out to one of our Project Leads or Maintainers. One month after every Bevy release Maintainers and Project Leads will evaluate the need for new roles, review candidates, and vote. Bringing in a new SME requires the support of the Project Leads and half of the Maintainers (however unanimous support is preferred). - -Check out the [Bevy People](https://bevyengine.org/community/people/#the-bevy-organization) page for the current list of SMEs. - -## Bevy Org Member / Triage Team - -[Bevy Org members](https://github.com/orgs/bevyengine/people) are contributors who: - -1. Have actively engaged with Bevy development. -2. Have demonstrated themselves to be polite and welcoming representatives of the project with an understanding of our goals and direction. -3. Have asked to join the Bevy Org. Reach out to @cart on [Discord](https://discord.gg/bevy) or email us at if you are interested. Everyone is welcome to do this. We generally accept membership requests, so don't hesitate if you are interested! - -All Bevy Org members are also Triage Team members. The Triage Team can label and close issues and PRs but do not have merge rights or any special authority within the community. - -## Role Rotation - -All Bevy Organization roles (excluding the Triage Team) have the potential for "role rotation". Roles like Project Lead, Maintainer, and SME are intentionally kept in limited supply to ensure a cohesive project vision. However these roles can be taxing, and qualified motivated people deserve a chance to lead. To resolve these issues, we plan on building in "role rotation". What this looks like hasn't yet been determined (as this issue hasn't come up yet and we are still in the process of scaling out our team), but we will try to appropriately balance the needs and desires of both current and future leaders, while also ensuring consistent vision and continuity for Bevy. - -Additionally, if you are currently holding a role that you can no longer "meaningfully engage with", please reach out to the Project Leads and Maintainers about rotating out. We intentionally keep leadership roles in short supply to make it easier to establish consensus and encourage a cohesive project vision. If you hold a role but don't engage with it, you are preventing other qualified people from driving the project forward. Note that leaving a role doesn't need to be permanent. If you need to rotate out because your life is currently busy with work / life / school / etc, but later you find more time, we can discuss rotating back in! diff --git a/errors/Cargo.toml b/errors/Cargo.toml index 32c7ca92c9ff4d..8e11214e7d8477 100644 --- a/errors/Cargo.toml +++ b/errors/Cargo.toml @@ -10,7 +10,3 @@ bevy = { path = ".." } [lints] workspace = true - -[package.metadata.docs.rs] -rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] -all-features = true diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index cf00f897a82980..37e81e40c2de26 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -1,16 +1,26 @@ //! Shows how to render simple primitive shapes with a single color. +//! +//! You can toggle wireframes with the space bar except on wasm. Wasm does not support +//! `POLYGON_MODE_LINE` on the gpu. +#[cfg(not(target_arch = "wasm32"))] +use bevy::sprite::{Wireframe2dConfig, Wireframe2dPlugin}; use bevy::{ prelude::*, - sprite::{MaterialMesh2dBundle, Mesh2dHandle, Wireframe2dConfig, Wireframe2dPlugin}, + sprite::{MaterialMesh2dBundle, Mesh2dHandle}, }; fn main() { - App::new() - .add_plugins((DefaultPlugins, Wireframe2dPlugin)) - .add_systems(Startup, setup) - .add_systems(Update, toggle_wireframe) - .run(); + let mut app = App::new(); + app.add_plugins(( + DefaultPlugins, + #[cfg(not(target_arch = "wasm32"))] + Wireframe2dPlugin, + )) + .add_systems(Startup, setup); + #[cfg(not(target_arch = "wasm32"))] + app.add_systems(Update, toggle_wireframe); + app.run(); } const X_EXTENT: f32 = 900.; @@ -57,6 +67,7 @@ fn setup( }); } + #[cfg(not(target_arch = "wasm32"))] commands.spawn( TextBundle::from_section("Press space to toggle wireframes", TextStyle::default()) .with_style(Style { @@ -68,6 +79,7 @@ fn setup( ); } +#[cfg(not(target_arch = "wasm32"))] fn toggle_wireframe( mut wireframe_config: ResMut, keyboard: Res>, diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 788649f9793c0e..45f759a5e4ede1 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -17,7 +17,11 @@ fn draw_cursor( ) { let (camera, camera_transform) = camera_query.single(); - let Some(cursor_position) = windows.single().cursor_position() else { + let Ok(window) = windows.get_single() else { + return; + }; + + let Some(cursor_position) = window.cursor_position() else { return; }; diff --git a/examples/2d/bounding_2d.rs b/examples/2d/bounding_2d.rs index 0807a4890cab7f..2025b00741f1e1 100644 --- a/examples/2d/bounding_2d.rs +++ b/examples/2d/bounding_2d.rs @@ -1,6 +1,10 @@ //! This example demonstrates bounding volume intersections. -use bevy::{color::palettes::css::*, math::bounding::*, prelude::*}; +use bevy::{ + color::palettes::css::*, + math::{bounding::*, Isometry2d}, + prelude::*, +}; fn main() { App::new() @@ -146,26 +150,27 @@ fn update_volumes( for (entity, desired_volume, shape, transform) in query.iter() { let translation = transform.translation.xy(); let rotation = transform.rotation.to_euler(EulerRot::YXZ).2; + let isometry = Isometry2d::new(translation, Rot2::radians(rotation)); match desired_volume { DesiredVolume::Aabb => { let aabb = match shape { - Shape::Rectangle(r) => r.aabb_2d(translation, rotation), - Shape::Circle(c) => c.aabb_2d(translation, rotation), - Shape::Triangle(t) => t.aabb_2d(translation, rotation), - Shape::Line(l) => l.aabb_2d(translation, rotation), - Shape::Capsule(c) => c.aabb_2d(translation, rotation), - Shape::Polygon(p) => p.aabb_2d(translation, rotation), + Shape::Rectangle(r) => r.aabb_2d(isometry), + Shape::Circle(c) => c.aabb_2d(isometry), + Shape::Triangle(t) => t.aabb_2d(isometry), + Shape::Line(l) => l.aabb_2d(isometry), + Shape::Capsule(c) => c.aabb_2d(isometry), + Shape::Polygon(p) => p.aabb_2d(isometry), }; commands.entity(entity).insert(CurrentVolume::Aabb(aabb)); } DesiredVolume::Circle => { let circle = match shape { - Shape::Rectangle(r) => r.bounding_circle(translation, rotation), - Shape::Circle(c) => c.bounding_circle(translation, rotation), - Shape::Triangle(t) => t.bounding_circle(translation, rotation), - Shape::Line(l) => l.bounding_circle(translation, rotation), - Shape::Capsule(c) => c.bounding_circle(translation, rotation), - Shape::Polygon(p) => p.bounding_circle(translation, rotation), + Shape::Rectangle(r) => r.bounding_circle(isometry), + Shape::Circle(c) => c.bounding_circle(isometry), + Shape::Triangle(t) => t.bounding_circle(isometry), + Shape::Line(l) => l.bounding_circle(isometry), + Shape::Capsule(c) => c.bounding_circle(isometry), + Shape::Polygon(p) => p.bounding_circle(isometry), }; commands .entity(entity) diff --git a/examples/2d/mesh2d_alpha_mode.rs b/examples/2d/mesh2d_alpha_mode.rs new file mode 100644 index 00000000000000..0cb8b5bd031451 --- /dev/null +++ b/examples/2d/mesh2d_alpha_mode.rs @@ -0,0 +1,97 @@ +//! This example is used to test how transforms interact with alpha modes for [`MaterialMesh2dBundle`] entities. +//! This makes sure the depth buffer is correctly being used for opaque and transparent 2d meshes + +use bevy::{ + color::palettes::css::{BLUE, GREEN, WHITE}, + prelude::*, + sprite::{AlphaMode2d, MaterialMesh2dBundle}, +}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(Camera2dBundle::default()); + + let texture_handle = asset_server.load("branding/icon.png"); + let mesh_handle = meshes.add(Rectangle::from_size(Vec2::splat(256.0))); + + // opaque + // Each sprite should be square with the transparent parts being completely black + // The blue sprite should be on top with the white and green one behind it + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: WHITE.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(-400.0, 0.0, 0.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: BLUE.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(-300.0, 0.0, 1.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: GREEN.into(), + alpha_mode: AlphaMode2d::Opaque, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(-200.0, 0.0, -1.0), + ..default() + }); + + // Test the interaction between opaque/mask and transparent meshes + // The white sprite should be: + // - only the icon is opaque but background is transparent + // - on top of the green sprite + // - behind the blue sprite + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: WHITE.into(), + alpha_mode: AlphaMode2d::Mask(0.5), + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(200.0, 0.0, 0.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: BLUE.with_alpha(0.7).into(), + alpha_mode: AlphaMode2d::Blend, + texture: Some(texture_handle.clone()), + }), + transform: Transform::from_xyz(300.0, 0.0, 1.0), + ..default() + }); + commands.spawn(MaterialMesh2dBundle { + mesh: mesh_handle.clone().into(), + material: materials.add(ColorMaterial { + color: GREEN.with_alpha(0.7).into(), + alpha_mode: AlphaMode2d::Blend, + texture: Some(texture_handle), + }), + transform: Transform::from_xyz(400.0, 0.0, -1.0), + ..default() + }); +} diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index f15421d449fc21..4b22417deb5aa1 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -5,7 +5,10 @@ use std::f32::consts::FRAC_PI_2; use bevy::{ color::palettes::css::{BLUE, DARK_SLATE_GREY, RED}, - math::bounding::{Bounded2d, BoundingVolume}, + math::{ + bounding::{Bounded2d, BoundingVolume}, + Isometry2d, + }, prelude::*, render::mesh::{CircularMeshUvMode, CircularSectorMeshBuilder, CircularSegmentMeshBuilder}, sprite::MaterialMesh2dBundle, @@ -114,11 +117,12 @@ fn draw_bounds( let (_, rotation, translation) = transform.to_scale_rotation_translation(); let translation = translation.truncate(); let rotation = rotation.to_euler(EulerRot::XYZ).2; + let isometry = Isometry2d::new(translation, Rot2::radians(rotation)); - let aabb = shape.0.aabb_2d(translation, rotation); + let aabb = shape.0.aabb_2d(isometry); gizmos.rect_2d(aabb.center(), 0.0, aabb.half_size() * 2.0, RED); - let bounding_circle = shape.0.bounding_circle(translation, rotation); + let bounding_circle = shape.0.bounding_circle(isometry); gizmos.circle_2d(bounding_circle.center, bounding_circle.radius(), BLUE); } } diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 75b018c3681d9a..6a7e97f6fd071b 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -7,20 +7,21 @@ use bevy::{ color::palettes::basic::YELLOW, - core_pipeline::core_2d::Transparent2d, + core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, math::FloatOrd, prelude::*, render::{ - mesh::{GpuMesh, Indices, MeshVertexAttribute}, + mesh::{Indices, MeshVertexAttribute, RenderMesh}, render_asset::{RenderAssetUsages, RenderAssets}, render_phase::{ AddRenderCommand, DrawFunctions, PhaseItemExtraIndex, SetItemPipeline, ViewSortedRenderPhases, }, render_resource::{ - BlendState, ColorTargetState, ColorWrites, Face, FragmentState, FrontFace, - MultisampleState, PipelineCache, PolygonMode, PrimitiveState, PrimitiveTopology, - RenderPipelineDescriptor, SpecializedRenderPipeline, SpecializedRenderPipelines, + BlendState, ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, + DepthStencilState, Face, FragmentState, FrontFace, MultisampleState, PipelineCache, + PolygonMode, PrimitiveState, PrimitiveTopology, RenderPipelineDescriptor, + SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilState, TextureFormat, VertexBufferLayout, VertexFormat, VertexState, VertexStepMode, }, texture::BevyDefault, @@ -198,7 +199,22 @@ impl SpecializedRenderPipeline for ColoredMesh2dPipeline { topology: key.primitive_topology(), strip_index_format: None, }, - depth_stencil: None, + depth_stencil: Some(DepthStencilState { + format: CORE_2D_DEPTH_FORMAT, + depth_write_enabled: false, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), multisample: MultisampleState { count: key.msaa_samples(), mask: !0, @@ -351,17 +367,16 @@ pub fn queue_colored_mesh2d( colored_mesh2d_pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - msaa: Res, - render_meshes: Res>, + render_meshes: Res>, render_mesh_instances: Res, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &VisibleEntities, &ExtractedView)>, + mut views: Query<(Entity, &VisibleEntities, &ExtractedView, &Msaa)>, ) { if render_mesh_instances.is_empty() { return; } // Iterate each view (a camera is a view) - for (view_entity, visible_entities, view) in &mut views { + for (view_entity, visible_entities, view, msaa) in &mut views { let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { continue; }; diff --git a/examples/2d/pixel_grid_snap.rs b/examples/2d/pixel_grid_snap.rs index 075dc06818f39f..c191ad8c345c3d 100644 --- a/examples/2d/pixel_grid_snap.rs +++ b/examples/2d/pixel_grid_snap.rs @@ -29,7 +29,6 @@ const HIGH_RES_LAYERS: RenderLayers = RenderLayers::layer(1); fn main() { App::new() .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) - .insert_resource(Msaa::Off) .add_systems(Startup, (setup_camera, setup_sprite, setup_mesh)) .add_systems(Update, (rotate, fit_canvas)) .run(); @@ -131,6 +130,7 @@ fn setup_camera(mut commands: Commands, mut images: ResMut>) { target: RenderTarget::Image(image_handle.clone()), ..default() }, + msaa: Msaa::Off, ..default() }, InGameCamera, @@ -149,7 +149,14 @@ fn setup_camera(mut commands: Commands, mut images: ResMut>) { // the "outer" camera renders whatever is on `HIGH_RES_LAYERS` to the screen. // here, the canvas and one of the sample sprites will be rendered by this camera - commands.spawn((Camera2dBundle::default(), OuterCamera, HIGH_RES_LAYERS)); + commands.spawn(( + Camera2dBundle { + msaa: Msaa::Off, + ..default() + }, + OuterCamera, + HIGH_RES_LAYERS, + )); } /// Rotates entities to demonstrate grid snapping. diff --git a/examples/2d/wireframe_2d.rs b/examples/2d/wireframe_2d.rs index 53d6e1b103f2b1..40052642d2e5a1 100644 --- a/examples/2d/wireframe_2d.rs +++ b/examples/2d/wireframe_2d.rs @@ -44,7 +44,7 @@ fn main() { global: true, // Controls the default color of all wireframes. Used as the default color for global wireframes. // Can be changed per mesh using the `Wireframe2dColor` component. - default_color: WHITE, + default_color: WHITE.into(), }) .add_systems(Startup, setup) .add_systems(Update, update_colors) @@ -91,7 +91,9 @@ fn setup( Wireframe2d, // This lets you configure the wireframe color of this entity. // If not set, this will use the color in `WireframeConfig` - Wireframe2dColor { color: GREEN }, + Wireframe2dColor { + color: GREEN.into(), + }, )); // Camera @@ -126,7 +128,8 @@ Wireframe2dConfig ------------- Global: {} Color: {:?}", - config.global, config.default_color, + config.global, + config.default_color.to_srgba(), ); // Toggle showing a wireframe on all meshes @@ -136,17 +139,21 @@ Color: {:?}", // Toggle the global wireframe color if keyboard_input.just_pressed(KeyCode::KeyX) { - config.default_color = if config.default_color == WHITE { - RED + config.default_color = if config.default_color == WHITE.into() { + RED.into() } else { - WHITE + WHITE.into() }; } // Toggle the color of a wireframe using `Wireframe2dColor` and not the global color if keyboard_input.just_pressed(KeyCode::KeyC) { for mut color in &mut wireframe_colors { - color.color = if color.color == GREEN { RED } else { GREEN }; + color.color = if color.color == GREEN.into() { + RED.into() + } else { + GREEN.into() + }; } } } diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index 63fbcfe3f311d3..6abac8371a287d 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -1,11 +1,15 @@ //! This example demonstrates the built-in 3d shapes in Bevy. //! The scene includes a patterned texture and a rotation for visualizing the normals and UVs. +//! +//! You can toggle wireframes with the space bar except on wasm. Wasm does not support +//! `POLYGON_MODE_LINE` on the gpu. use std::f32::consts::PI; +#[cfg(not(target_arch = "wasm32"))] +use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; use bevy::{ color::palettes::basic::SILVER, - pbr::wireframe::{WireframeConfig, WireframePlugin}, prelude::*, render::{ render_asset::RenderAssetUsages, @@ -17,10 +21,18 @@ fn main() { App::new() .add_plugins(( DefaultPlugins.set(ImagePlugin::default_nearest()), + #[cfg(not(target_arch = "wasm32"))] WireframePlugin, )) .add_systems(Startup, setup) - .add_systems(Update, (rotate, toggle_wireframe)) + .add_systems( + Update, + ( + rotate, + #[cfg(not(target_arch = "wasm32"))] + toggle_wireframe, + ), + ) .run(); } @@ -128,6 +140,7 @@ fn setup( ..default() }); + #[cfg(not(target_arch = "wasm32"))] commands.spawn( TextBundle::from_section("Press space to toggle wireframes", TextStyle::default()) .with_style(Style { @@ -174,6 +187,7 @@ fn uv_debug_texture() -> Image { ) } +#[cfg(not(target_arch = "wasm32"))] fn toggle_wireframe( mut wireframe_config: ResMut, keyboard: Res>, diff --git a/examples/3d/animated_material.rs b/examples/3d/animated_material.rs index ecf18cea04a030..131284568ddc54 100644 --- a/examples/3d/animated_material.rs +++ b/examples/3d/animated_material.rs @@ -26,6 +26,7 @@ fn setup( diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 2_000.0, + ..default() }, )); diff --git a/examples/3d/anisotropy.rs b/examples/3d/anisotropy.rs index f5856daf86df2b..84583c19b1c37a 100644 --- a/examples/3d/anisotropy.rs +++ b/examples/3d/anisotropy.rs @@ -240,11 +240,13 @@ fn add_skybox_and_environment_map( .insert(Skybox { brightness: 5000.0, image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + ..default() }) .insert(EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 2500.0, + ..default() }); } diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index 7b04a499a0bcff..b5e785b95385d6 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -23,7 +23,6 @@ use bevy::{ fn main() { App::new() - .insert_resource(Msaa::Off) .add_plugins((DefaultPlugins, TemporalAntiAliasPlugin)) .add_systems(Startup, setup) .add_systems(Update, (modify_aa, modify_sharpening, update_ui)) @@ -38,13 +37,13 @@ fn modify_aa( Option<&mut Fxaa>, Option<&mut SmaaSettings>, Option<&TemporalAntiAliasSettings>, + &mut Msaa, ), With, >, - mut msaa: ResMut, mut commands: Commands, ) { - let (camera_entity, fxaa, smaa, taa) = camera.single_mut(); + let (camera_entity, fxaa, smaa, taa, mut msaa) = camera.single_mut(); let mut camera = commands.entity(camera_entity); // No AA @@ -176,13 +175,13 @@ fn update_ui( Option<&SmaaSettings>, Option<&TemporalAntiAliasSettings>, &ContrastAdaptiveSharpeningSettings, + &Msaa, ), With, >, - msaa: Res, mut ui: Query<&mut Text>, ) { - let (fxaa, smaa, taa, cas_settings) = camera.single(); + let (fxaa, smaa, taa, cas_settings, msaa) = camera.single(); let mut ui = ui.single_mut(); let ui = &mut ui.sections[0].value; @@ -324,6 +323,7 @@ fn setup( diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 150.0, + ..default() }, FogSettings { color: Color::srgba_u8(43, 44, 47, 255), diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index d59b3d820f9b1b..6984ea27ea63c9 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -53,7 +53,8 @@ fn setup( }, Skybox { image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), - brightness: bevy::pbr::light_consts::lux::DIRECT_SUNLIGHT, + brightness: light_consts::lux::DIRECT_SUNLIGHT, + ..default() }, )); diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs index ee6c2f3b1742ce..521494446c6e7b 100644 --- a/examples/3d/blend_modes.rs +++ b/examples/3d/blend_modes.rs @@ -21,7 +21,7 @@ fn main() { .add_systems(Update, example_control_system); // Unfortunately, MSAA and HDR are not supported simultaneously under WebGL. - // Since this example uses HDR, we must disable MSAA for WASM builds, at least + // Since this example uses HDR, we must disable MSAA for Wasm builds, at least // until WebGPU is ready and no longer behind a feature flag in Web browsers. #[cfg(target_arch = "wasm32")] app.insert_resource(Msaa::Off); diff --git a/examples/3d/clearcoat.rs b/examples/3d/clearcoat.rs index 5d977eba7dfc64..69aaa3071795bb 100644 --- a/examples/3d/clearcoat.rs +++ b/examples/3d/clearcoat.rs @@ -224,11 +224,13 @@ fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { .insert(Skybox { brightness: 5000.0, image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + ..default() }) .insert(EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 2000.0, + ..default() }); } diff --git a/examples/3d/color_grading.rs b/examples/3d/color_grading.rs index cebaadafefa03e..6ed6d12d8a48e8 100644 --- a/examples/3d/color_grading.rs +++ b/examples/3d/color_grading.rs @@ -374,6 +374,7 @@ fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 2000.0, + ..default() }, )); } diff --git a/examples/3d/deferred_rendering.rs b/examples/3d/deferred_rendering.rs index 7bd5f8937b06f5..1de231aebb2774 100644 --- a/examples/3d/deferred_rendering.rs +++ b/examples/3d/deferred_rendering.rs @@ -17,7 +17,6 @@ use bevy::{ fn main() { App::new() - .insert_resource(Msaa::Off) .insert_resource(DefaultOpaqueRendererMethod::deferred()) .insert_resource(DirectionalLightShadowMap { size: 4096 }) .add_plugins(DefaultPlugins) @@ -42,6 +41,8 @@ fn setup( }, transform: Transform::from_xyz(0.7, 0.7, 1.0) .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + // MSAA needs to be off for Deferred rendering + msaa: Msaa::Off, ..default() }, FogSettings { @@ -56,6 +57,7 @@ fn setup( diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 2000.0, + ..default() }, DepthPrepass, MotionVectorPrepass, diff --git a/examples/3d/fog_volumes.rs b/examples/3d/fog_volumes.rs new file mode 100644 index 00000000000000..40818a2f93cc12 --- /dev/null +++ b/examples/3d/fog_volumes.rs @@ -0,0 +1,89 @@ +//! Demonstrates fog volumes with voxel density textures. +//! +//! We render the Stanford bunny as a fog volume. Parts of the bunny become +//! lighter and darker as the camera rotates. This is physically-accurate +//! behavior that results from the scattering and absorption of the directional +//! light. + +use bevy::{ + math::vec3, + pbr::{FogVolume, VolumetricFogSettings, VolumetricLight}, + prelude::*, +}; + +/// Entry point. +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Fog Volumes Example".into(), + ..default() + }), + ..default() + })) + .insert_resource(AmbientLight::NONE) + .add_systems(Startup, setup) + .add_systems(Update, rotate_camera) + .run(); +} + +/// Spawns all the objects in the scene. +fn setup(mut commands: Commands, asset_server: Res) { + // Spawn a fog volume with a voxelized version of the Stanford bunny. + commands + .spawn(SpatialBundle { + visibility: Visibility::Visible, + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }) + .insert(FogVolume { + density_texture: Some(asset_server.load("volumes/bunny.ktx2")), + density_factor: 1.0, + // Scatter as much of the light as possible, to brighten the bunny + // up. + scattering: 1.0, + ..default() + }); + + // Spawn a bright directional light that illuminates the fog well. + commands + .spawn(DirectionalLightBundle { + transform: Transform::from_xyz(1.0, 1.0, -0.3).looking_at(vec3(0.0, 0.5, 0.0), Vec3::Y), + directional_light: DirectionalLight { + shadows_enabled: true, + illuminance: 32000.0, + ..default() + }, + ..default() + }) + // Make sure to add this for the light to interact with the fog. + .insert(VolumetricLight); + + // Spawn a camera. + commands + .spawn(Camera3dBundle { + transform: Transform::from_xyz(-0.75, 1.0, 2.0) + .looking_at(vec3(0.0, 0.0, 0.0), Vec3::Y), + camera: Camera { + hdr: true, + ..default() + }, + ..default() + }) + .insert(VolumetricFogSettings { + // Make this relatively high in order to increase the fog quality. + step_count: 64, + // Disable ambient light. + ambient_intensity: 0.0, + ..default() + }); +} + +/// Rotates the camera a bit every frame. +fn rotate_camera(mut cameras: Query<&mut Transform, With>) { + for mut camera_transform in cameras.iter_mut() { + *camera_transform = + Transform::from_translation(Quat::from_rotation_y(0.01) * camera_transform.translation) + .looking_at(vec3(0.0, 0.5, 0.0), Vec3::Y); + } +} diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 40c604abbb1b56..0c4c44f00bf60e 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -239,6 +239,7 @@ fn spawn_camera(commands: &mut Commands, assets: &ExampleAssets) { .insert(Skybox { image: assets.skybox.clone(), brightness: 150.0, + ..default() }); } diff --git a/examples/3d/load_gltf.rs b/examples/3d/load_gltf.rs index 358446238e1b11..b8579c76faea73 100644 --- a/examples/3d/load_gltf.rs +++ b/examples/3d/load_gltf.rs @@ -26,6 +26,7 @@ fn setup(mut commands: Commands, asset_server: Res) { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 250.0, + ..default() }, )); diff --git a/examples/3d/load_gltf_extras.rs b/examples/3d/load_gltf_extras.rs index e32dcbd5f4a094..4cbfcd39294cc1 100644 --- a/examples/3d/load_gltf_extras.rs +++ b/examples/3d/load_gltf_extras.rs @@ -29,9 +29,11 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }); + // a barebones scene containing one of each gltf_extra type commands.spawn(SceneBundle { - scene: asset_server.load("models/extras/gltf_extras.glb#Scene0"), + scene: asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/extras/gltf_extras.glb")), ..default() }); diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index ecd32019181985..4721e0c9819df8 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -16,7 +16,8 @@ use bevy::{ use camera_controller::{CameraController, CameraControllerPlugin}; use std::{f32::consts::PI, path::Path, process::ExitCode}; -const ASSET_URL: &str = "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/bd869887bc5c9c6e74e353f657d342bef84bacd8/bunny.meshlet_mesh"; +const ASSET_URL: &str = + "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh"; fn main() -> ExitCode { if !Path::new("./assets/models/bunny.meshlet_mesh").exists() { @@ -49,12 +50,14 @@ fn setup( Camera3dBundle { transform: Transform::from_translation(Vec3::new(1.8, 0.4, -0.1)) .looking_at(Vec3::ZERO, Vec3::Y), + msaa: Msaa::Off, ..default() }, EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 150.0, + ..default() }, CameraController::default(), )); diff --git a/examples/3d/pbr.rs b/examples/3d/pbr.rs index e8e4d0dc8cf9fd..3c7a76211903f1 100644 --- a/examples/3d/pbr.rs +++ b/examples/3d/pbr.rs @@ -130,6 +130,7 @@ fn setup( diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 900.0, + ..default() }, )); } diff --git a/examples/3d/post_processing.rs b/examples/3d/post_processing.rs new file mode 100644 index 00000000000000..db011aa7fe6eb0 --- /dev/null +++ b/examples/3d/post_processing.rs @@ -0,0 +1,217 @@ +//! Demonstrates Bevy's built-in postprocessing features. +//! +//! Currently, this simply consists of chromatic aberration. + +use std::f32::consts::PI; + +use bevy::{ + core_pipeline::post_process::ChromaticAberration, pbr::CascadeShadowConfigBuilder, prelude::*, +}; + +/// The number of units per frame to add to or subtract from intensity when the +/// arrow keys are held. +const CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED: f32 = 0.002; + +/// The maximum supported chromatic aberration intensity level. +const MAX_CHROMATIC_ABERRATION_INTENSITY: f32 = 0.4; + +/// The settings that the user can control. +#[derive(Resource)] +struct AppSettings { + /// The intensity of the chromatic aberration effect. + chromatic_aberration_intensity: f32, +} + +/// The entry point. +fn main() { + App::new() + .init_resource::() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Chromatic Aberration Example".into(), + ..default() + }), + ..default() + })) + .add_systems(Startup, setup) + .add_systems(Update, handle_keyboard_input) + .add_systems( + Update, + (update_chromatic_aberration_settings, update_help_text) + .run_if(resource_changed::) + .after(handle_keyboard_input), + ) + .run(); +} + +/// Creates the example scene and spawns the UI. +fn setup(mut commands: Commands, asset_server: Res, app_settings: Res) { + // Spawn the camera. + spawn_camera(&mut commands, &asset_server); + + // Create the scene. + spawn_scene(&mut commands, &asset_server); + + // Spawn the help text. + spawn_text(&mut commands, &app_settings); +} + +/// Spawns the camera, including the [`ChromaticAberration`] component. +fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { + commands.spawn(( + Camera3dBundle { + camera: Camera { + hdr: true, + ..default() + }, + transform: Transform::from_xyz(0.7, 0.7, 1.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + ..default() + }, + FogSettings { + color: Color::srgb_u8(43, 44, 47), + falloff: FogFalloff::Linear { + start: 1.0, + end: 8.0, + }, + ..default() + }, + EnvironmentMapLight { + diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), + specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), + intensity: 2000.0, + ..default() + }, + // Include the `ChromaticAberration` component. + ChromaticAberration::default(), + )); +} + +/// Spawns the scene. +/// +/// This is just the tonemapping test scene, chosen for the fact that it uses a +/// variety of colors. +fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) { + // Spawn the main scene. + commands.spawn(SceneBundle { + scene: asset_server.load( + GltfAssetLabel::Scene(0).from_asset("models/TonemappingTest/TonemappingTest.gltf"), + ), + ..default() + }); + + // Spawn the flight helmet. + commands.spawn(SceneBundle { + scene: asset_server + .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")), + transform: Transform::from_xyz(0.5, 0.0, -0.5) + .with_rotation(Quat::from_rotation_y(-0.15 * PI)), + ..default() + }); + + // Spawn the light. + commands.spawn(DirectionalLightBundle { + directional_light: DirectionalLight { + illuminance: 15000.0, + shadows_enabled: true, + ..default() + }, + transform: Transform::from_rotation(Quat::from_euler( + EulerRot::ZYX, + 0.0, + PI * -0.15, + PI * -0.15, + )), + cascade_shadow_config: CascadeShadowConfigBuilder { + maximum_distance: 3.0, + first_cascade_far_bound: 0.9, + ..default() + } + .into(), + ..default() + }); +} + +/// Spawns the help text at the bottom of the screen. +fn spawn_text(commands: &mut Commands, app_settings: &AppSettings) { + commands.spawn( + TextBundle { + text: create_help_text(app_settings), + ..default() + } + .with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }), + ); +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + chromatic_aberration_intensity: ChromaticAberration::default().intensity, + } + } +} + +/// Creates help text at the bottom of the screen. +fn create_help_text(app_settings: &AppSettings) -> Text { + Text::from_section( + format!( + "Chromatic aberration intensity: {} (Press Left or Right to change)", + app_settings.chromatic_aberration_intensity + ), + TextStyle::default(), + ) +} + +/// Handles requests from the user to change the chromatic aberration intensity. +fn handle_keyboard_input(mut app_settings: ResMut, input: Res>) { + let mut delta = 0.0; + if input.pressed(KeyCode::ArrowLeft) { + delta -= CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED; + } else if input.pressed(KeyCode::ArrowRight) { + delta += CHROMATIC_ABERRATION_INTENSITY_ADJUSTMENT_SPEED; + } + + // If no arrow key was pressed, just bail out. + if delta == 0.0 { + return; + } + + app_settings.chromatic_aberration_intensity = (app_settings.chromatic_aberration_intensity + + delta) + .clamp(0.0, MAX_CHROMATIC_ABERRATION_INTENSITY); +} + +/// Updates the [`ChromaticAberration`] settings per the [`AppSettings`]. +fn update_chromatic_aberration_settings( + mut chromatic_aberration_settings: Query<&mut ChromaticAberration>, + app_settings: Res, +) { + let intensity = app_settings.chromatic_aberration_intensity; + + // Pick a reasonable maximum sample size for the intensity to avoid an + // artifact whereby the individual samples appear instead of producing + // smooth streaks of color. + // + // Don't take this formula too seriously; it hasn't been heavily tuned. + let max_samples = ((intensity - 0.02) / (0.20 - 0.02) * 56.0 + 8.0) + .clamp(8.0, 64.0) + .round() as u32; + + for mut chromatic_aberration_settings in &mut chromatic_aberration_settings { + chromatic_aberration_settings.intensity = intensity; + chromatic_aberration_settings.max_samples = max_samples; + } +} + +/// Updates the help text at the bottom of the screen to reflect the current +/// [`AppSettings`]. +fn update_help_text(mut text: Query<&mut Text>, app_settings: Res) { + for mut text in text.iter_mut() { + *text = create_help_text(&app_settings); + } +} diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs index 3184982cb486fa..ca8f02441dd257 100644 --- a/examples/3d/reflection_probes.rs +++ b/examples/3d/reflection_probes.rs @@ -151,6 +151,7 @@ fn spawn_reflection_probe(commands: &mut Commands, cubemaps: &Cubemaps) { diffuse_map: cubemaps.diffuse.clone(), specular_map: cubemaps.specular_reflection_probe.clone(), intensity: 5000.0, + ..default() }, }); } @@ -187,6 +188,7 @@ fn add_environment_map_to_camera( .insert(Skybox { image: cubemaps.skybox.clone(), brightness: 5000.0, + ..default() }); } } @@ -298,6 +300,7 @@ fn create_camera_environment_map_light(cubemaps: &Cubemaps) -> EnvironmentMapLig diffuse_map: cubemaps.diffuse.clone(), specular_map: cubemaps.specular_environment_map.clone(), intensity: 5000.0, + ..default() } } diff --git a/examples/3d/rotate_environment_map.rs b/examples/3d/rotate_environment_map.rs new file mode 100644 index 00000000000000..dd128959299cd0 --- /dev/null +++ b/examples/3d/rotate_environment_map.rs @@ -0,0 +1,124 @@ +//! Demonstrates how to rotate the skybox and the environment map simultaneously. + +use std::f32::consts::PI; + +use bevy::{ + color::palettes::css::{GOLD, WHITE}, + core_pipeline::{tonemapping::Tonemapping::AcesFitted, Skybox}, + prelude::*, + render::texture::ImageLoaderSettings, +}; + +/// Entry point. +pub fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, rotate_skybox_and_environment_map) + .run(); +} + +/// Initializes the scene. +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + let sphere_mesh = create_sphere_mesh(&mut meshes); + spawn_sphere(&mut commands, &mut materials, &asset_server, &sphere_mesh); + spawn_light(&mut commands); + spawn_camera(&mut commands, &asset_server); +} + +/// Rotate the skybox and the environment map per frame. +fn rotate_skybox_and_environment_map( + mut environments: Query<(&mut Skybox, &mut EnvironmentMapLight)>, + time: Res

for DrawMeshInstanced { - type Param = (SRes>, SRes); + type Param = ( + SRes>, + SRes, + SRes, + ); type ViewQuery = (); type ItemQuery = Read; @@ -250,33 +256,50 @@ impl RenderCommand

for DrawMeshInstanced { item: &P, _view: (), instance_buffer: Option<&'w InstanceBuffer>, - (meshes, render_mesh_instances): SystemParamItem<'w, '_, Self::Param>, + (meshes, render_mesh_instances, mesh_allocator): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { + // A borrow check workaround. + let mesh_allocator = mesh_allocator.into_inner(); + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(item.entity()) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(gpu_mesh) = meshes.into_inner().get(mesh_instance.mesh_asset_id) else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; }; let Some(instance_buffer) = instance_buffer else { - return RenderCommandResult::Failure; + return RenderCommandResult::Skip; + }; + let Some(vertex_buffer_slice) = + mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id) + else { + return RenderCommandResult::Skip; }; - pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..)); + pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); pass.set_vertex_buffer(1, instance_buffer.buffer.slice(..)); match &gpu_mesh.buffer_info { - GpuBufferInfo::Indexed { - buffer, + RenderMeshBufferInfo::Indexed { index_format, count, } => { - pass.set_index_buffer(buffer.slice(..), 0, *index_format); - pass.draw_indexed(0..*count, 0, 0..instance_buffer.length as u32); + let Some(index_buffer_slice) = + mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id) + else { + return RenderCommandResult::Skip; + }; + + pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); + pass.draw_indexed( + index_buffer_slice.range.start..(index_buffer_slice.range.start + count), + vertex_buffer_slice.range.start as i32, + 0..instance_buffer.length as u32, + ); } - GpuBufferInfo::NonIndexed => { + RenderMeshBufferInfo::NonIndexed => { pass.draw(0..gpu_mesh.vertex_count, 0..instance_buffer.length as u32); } } diff --git a/examples/shader/shader_prepass.rs b/examples/shader/shader_prepass.rs index b3e4b4ab256e6a..82515640ec1472 100644 --- a/examples/shader/shader_prepass.rs +++ b/examples/shader/shader_prepass.rs @@ -34,8 +34,6 @@ fn main() { )) .add_systems(Startup, setup) .add_systems(Update, (rotate, toggle_prepass_view)) - // Disabling MSAA for maximum compatibility. Shader prepass with MSAA needs GPU capability MULTISAMPLED_SHADING - .insert_resource(Msaa::Off) .run(); } @@ -52,6 +50,8 @@ fn setup( commands.spawn(( Camera3dBundle { transform: Transform::from_xyz(-2.0, 3., 5.0).looking_at(Vec3::ZERO, Vec3::Y), + // Disabling MSAA for maximum compatibility. Shader prepass with MSAA needs GPU capability MULTISAMPLED_SHADING + msaa: Msaa::Off, ..default() }, // To enable the prepass you need to add the components associated with the ones you need diff --git a/examples/shader/specialized_mesh_pipeline.rs b/examples/shader/specialized_mesh_pipeline.rs new file mode 100644 index 00000000000000..cb0cf10024073f --- /dev/null +++ b/examples/shader/specialized_mesh_pipeline.rs @@ -0,0 +1,358 @@ +//! Demonstrates how to define and use specialized mesh pipeline +//! +//! This example shows how to use the built-in [`SpecializedMeshPipeline`] +//! functionality with a custom [`RenderCommand`] to allow custom mesh rendering with +//! more flexibility than the material api. +//! +//! [`SpecializedMeshPipeline`] let's you customize the entire pipeline used when rendering a mesh. + +use bevy::{ + core_pipeline::core_3d::{Opaque3d, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT}, + math::{vec3, vec4}, + pbr::{ + DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, RenderMeshInstances, + SetMeshBindGroup, SetMeshViewBindGroup, + }, + prelude::*, + render::{ + extract_component::{ExtractComponent, ExtractComponentPlugin}, + mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology, RenderMesh}, + render_asset::{RenderAssetUsages, RenderAssets}, + render_phase::{ + AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, SetItemPipeline, + ViewBinnedRenderPhases, + }, + render_resource::{ + ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, Face, FragmentState, + FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState, + RenderPipelineDescriptor, SpecializedMeshPipeline, SpecializedMeshPipelineError, + SpecializedMeshPipelines, TextureFormat, VertexState, + }, + texture::BevyDefault as _, + view::{self, ExtractedView, ViewTarget, VisibilitySystems, VisibleEntities}, + Render, RenderApp, RenderSet, + }, +}; + +const SHADER_ASSET_PATH: &str = "shaders/specialized_mesh_pipeline.wgsl"; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(CustomRenderedMeshPipelinePlugin) + .add_systems(Startup, setup) + .run(); +} + +/// Spawns the objects in the scene. +fn setup(mut commands: Commands, mut meshes: ResMut>) { + // Build a custom triangle mesh with colors + // We define a custom mesh because the examples only uses a limited + // set of vertex attributes for simplicity + let mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(vec![0, 1, 2, 0, 2, 3])) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![ + vec3(-0.5, -0.5, 0.0), + vec3(0.5, -0.5, 0.0), + vec3(0.0, 0.25, 0.0), + ], + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_COLOR, + vec![ + vec4(1.0, 0.0, 0.0, 1.0), + vec4(0.0, 1.0, 0.0, 1.0), + vec4(0.0, 0.0, 1.0, 1.0), + ], + ); + + // spawn 3 triangles to show that batching works + for (x, y) in [-0.5, 0.0, 0.5].into_iter().zip([-0.25, 0.5, -0.25]) { + // Spawn an entity with all the required components for it to be rendered with our custom pipeline + commands.spawn(( + // We use a marker component to identify the mesh that will be rendered + // with our specialized pipeline + CustomRenderedEntity, + // We need to add the mesh handle to the entity + meshes.add(mesh.clone()), + // This bundle's components are needed for something to be rendered + SpatialBundle { + transform: Transform::from_xyz(x, y, 0.0), + ..SpatialBundle::INHERITED_IDENTITY + }, + )); + } + + // Spawn the camera. + commands.spawn(Camera3dBundle { + // Move the camera back a bit to see all the triangles + transform: Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); +} + +// When writing custom rendering code it's generally recommended to use a plugin. +// The main reason for this is that it gives you access to the finish() hook +// which is called after rendering resources are initialized. +struct CustomRenderedMeshPipelinePlugin; +impl Plugin for CustomRenderedMeshPipelinePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(ExtractComponentPlugin::::default()) + .add_systems( + PostUpdate, + // Make sure to tell Bevy to check our entity for visibility. Bevy won't + // do this by default, for efficiency reasons. + // This will do things like frustum culling and hierarchy visibility + view::check_visibility:: + .in_set(VisibilitySystems::CheckVisibility), + ); + + // We make sure to add these to the render app, not the main app. + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app + // This is needed to tell bevy about your custom pipeline + .init_resource::>() + // We need to use a custom draw command so we need to register it + .add_render_command::() + .add_systems(Render, queue_custom_mesh_pipeline.in_set(RenderSet::Queue)); + } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + // Creating this pipeline needs the RenderDevice and RenderQueue + // which are only available once rendering plugins are initialized. + render_app.init_resource::(); + } +} + +/// A marker component that represents an entity that is to be rendered using +/// our specialized pipeline. +/// +/// Note the [`ExtractComponent`] trait implementation. This is necessary to +/// tell Bevy that this object should be pulled into the render world. +#[derive(Clone, Component, ExtractComponent)] +struct CustomRenderedEntity; + +/// The custom draw commands that Bevy executes for each entity we enqueue into +/// the render phase. +type DrawSpecializedPipelineCommands = ( + // Set the pipeline + SetItemPipeline, + // Set the view uniform at bind group 0 + SetMeshViewBindGroup<0>, + // Set the mesh uniform at bind group 1 + SetMeshBindGroup<1>, + // Draw the mesh + DrawMesh, +); + +/// A query filter that tells [`view::check_visibility`] about our custom +/// rendered entity. +type WithCustomRenderedEntity = With; + +// This contains the state needed to speciazlize a mesh pipeline +#[derive(Resource)] +struct CustomMeshPipeline { + /// The base mesh pipeline defined by bevy + /// + /// This isn't required, but if you want to use a bevy `Mesh` it's easier when you + /// have access to the base `MeshPipeline` that bevy already defines + mesh_pipeline: MeshPipeline, + /// Stores the shader used for this pipeline directly on the pipeline. + /// This isn't required, it's only done like this for simplicity. + shader_handle: Handle, +} +impl FromWorld for CustomMeshPipeline { + fn from_world(world: &mut World) -> Self { + // Load the shader + let shader_handle: Handle = world.resource::().load(SHADER_ASSET_PATH); + Self { + mesh_pipeline: MeshPipeline::from_world(world), + shader_handle, + } + } +} + +impl SpecializedMeshPipeline for CustomMeshPipeline { + /// Pipeline use keys to determine how to specialize it. + /// The key is also used by the pipeline cache to determine if + /// it needs to create a new pipeline or not + /// + /// In this example we just use the base `MeshPipelineKey` defined by bevy, but this could be anything. + /// For example, if you want to make a pipeline with a procedural shader you could add the Handle to the key. + type Key = MeshPipelineKey; + + fn specialize( + &self, + mesh_key: Self::Key, + layout: &MeshVertexBufferLayoutRef, + ) -> Result { + // Define the vertex attributes based on a standard bevy [`Mesh`] + let mut vertex_attributes = Vec::new(); + if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + // Make sure this matches the shader location + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { + // Make sure this matches the shader location + vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(1)); + } + // This will automatically generate the correct `VertexBufferLayout` based on the vertex attributes + let vertex_buffer_layout = layout.0.get_layout(&vertex_attributes)?; + + Ok(RenderPipelineDescriptor { + label: Some("Specialized Mesh Pipeline".into()), + layout: vec![ + // Bind group 0 is the view uniform + self.mesh_pipeline + .get_view_layout(MeshPipelineViewLayoutKey::from(mesh_key)) + .clone(), + // Bind group 1 is the mesh uniform + self.mesh_pipeline.mesh_layouts.model_only.clone(), + ], + push_constant_ranges: vec![], + vertex: VertexState { + shader: self.shader_handle.clone(), + shader_defs: vec![], + entry_point: "vertex".into(), + // Customize how to store the meshes' vertex attributes in the vertex buffer + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: self.shader_handle.clone(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + // This isn't required, but bevy supports HDR and non-HDR rendering + // so it's generally recommended to specialize the pipeline for that + format: if mesh_key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + // For this example we only use opaque meshes, + // but if you wanted to use alpha blending you would need to set it here + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + primitive: PrimitiveState { + topology: mesh_key.primitive_topology(), + front_face: FrontFace::Ccw, + cull_mode: Some(Face::Back), + polygon_mode: PolygonMode::Fill, + ..default() + }, + // Note that if your view has no depth buffer this will need to be + // changed. + depth_stencil: Some(DepthStencilState { + format: CORE_3D_DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: default(), + bias: default(), + }), + // It's generally recommended to specialize your pipeline for MSAA, + // but it's not always possible + multisample: MultisampleState { + count: mesh_key.msaa_samples(), + ..MultisampleState::default() + }, + }) + } +} + +/// A render-world system that enqueues the entity with custom rendering into +/// the opaque render phases of each view. +#[allow(clippy::too_many_arguments)] +fn queue_custom_mesh_pipeline( + pipeline_cache: Res, + custom_mesh_pipeline: Res, + mut opaque_render_phases: ResMut>, + opaque_draw_functions: Res>, + mut specialized_mesh_pipelines: ResMut>, + views: Query<(Entity, &VisibleEntities, &ExtractedView, &Msaa), With>, + render_meshes: Res>, + render_mesh_instances: Res, +) { + // Get the id for our custom draw function + let draw_function_id = opaque_draw_functions + .read() + .id::(); + + // Render phases are per-view, so we need to iterate over all views so that + // the entity appears in them. (In this example, we have only one view, but + // it's good practice to loop over all views anyway.) + for (view_entity, view_visible_entities, view, msaa) in views.iter() { + let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else { + continue; + }; + + // Create the key based on the view. In this case we only care about MSAA and HDR + let view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); + + // Find all the custom rendered entities that are visible from this + // view. + for &visible_entity in view_visible_entities + .get::() + .iter() + { + // Get the mesh instance + let Some(mesh_instance) = render_mesh_instances.render_mesh_queue_data(visible_entity) + else { + continue; + }; + + // Get the mesh data + let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { + continue; + }; + + // Specialize the key for the current mesh entity + // For this example we only specialize based on the mesh topology + // but you could have more complex keys and that's where you'd need to create those keys + let mut mesh_key = view_key; + mesh_key |= MeshPipelineKey::from_primitive_topology(mesh.primitive_topology()); + + // Finally, we can specialize the pipeline based on the key + let pipeline_id = specialized_mesh_pipelines + .specialize( + &pipeline_cache, + &custom_mesh_pipeline, + mesh_key, + &mesh.layout, + ) + // This should never with this example, but if your pipeline specialization + // can fail you need to handle the error here + .expect("Failed to specialize mesh pipeline"); + + // Add the mesh with our specialized pipeline + opaque_phase.add( + Opaque3dBinKey { + draw_function: draw_function_id, + pipeline: pipeline_id, + // The asset ID is arbitrary; we simply use [`AssetId::invalid`], + // but you can use anything you like. Note that the asset ID need + // not be the ID of a [`Mesh`]. + asset_id: AssetId::::invalid().untyped(), + material_bind_group_id: None, + lightmap_image: None, + }, + visible_entity, + // This example supports batching, but if your pipeline doesn't + // support it you can use `BinnedRenderPhaseType::UnbatchableMesh` + BinnedRenderPhaseType::BatchableMesh, + ); + } + } +} diff --git a/examples/stress_tests/bevymark.rs b/examples/stress_tests/bevymark.rs index 067f761507cd93..e0959cc930eb17 100644 --- a/examples/stress_tests/bevymark.rs +++ b/examples/stress_tests/bevymark.rs @@ -13,7 +13,7 @@ use bevy::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, }, - sprite::{MaterialMesh2dBundle, Mesh2dHandle}, + sprite::{AlphaMode2d, MaterialMesh2dBundle, Mesh2dHandle}, utils::Duration, window::{PresentMode, WindowResolution}, winit::{UpdateMode, WinitSettings}, @@ -71,6 +71,10 @@ struct Args { /// generate z values in increasing order rather than randomly #[argh(switch)] ordered_z: bool, + + /// the alpha mode used to spawn the sprites + #[argh(option, default = "AlphaMode::Blend")] + alpha_mode: AlphaMode, } #[derive(Default, Clone)] @@ -94,6 +98,29 @@ impl FromStr for Mode { } } +#[derive(Default, Clone)] +enum AlphaMode { + Opaque, + #[default] + Blend, + AlphaMask, +} + +impl FromStr for AlphaMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "opaque" => Ok(Self::Opaque), + "blend" => Ok(Self::Blend), + "alpha_mask" => Ok(Self::AlphaMask), + _ => Err(format!( + "Unknown alpha mode: '{s}', valid modes: 'opaque', 'blend', 'alpha_mask'" + )), + } + } +} + const FIXED_TIMESTEP: f32 = 0.2; fn main() { @@ -573,10 +600,17 @@ fn init_materials( } .max(1); + let alpha_mode = match args.alpha_mode { + AlphaMode::Opaque => AlphaMode2d::Opaque, + AlphaMode::Blend => AlphaMode2d::Blend, + AlphaMode::AlphaMask => AlphaMode2d::Mask(0.5), + }; + let mut materials = Vec::with_capacity(capacity); materials.push(assets.add(ColorMaterial { color: Color::WHITE, texture: textures.first().cloned(), + alpha_mode, })); // We're seeding the PRNG here to make this example deterministic for testing purposes. @@ -588,6 +622,7 @@ fn init_materials( assets.add(ColorMaterial { color: Color::srgb_u8(color_rng.gen(), color_rng.gen(), color_rng.gen()), texture: textures.choose(&mut texture_rng).cloned(), + alpha_mode, }) }) .take(capacity - materials.len()), diff --git a/examples/stress_tests/transform_hierarchy.rs b/examples/stress_tests/transform_hierarchy.rs index 3e6064971e79af..404dc4cf82d342 100644 --- a/examples/stress_tests/transform_hierarchy.rs +++ b/examples/stress_tests/transform_hierarchy.rs @@ -18,7 +18,11 @@ //! | `humanoids_inactive` | 4000 humanoid rigs. Only 10 are active. | //! | `humanoids_mixed` | 2000 active and 2000 inactive humanoid rigs. | -use bevy::prelude::*; +use bevy::{ + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::*, + window::ExitCondition, +}; use rand::Rng; /// pre-defined test configurations with name @@ -183,7 +187,15 @@ fn main() { App::new() .insert_resource(cfg) - .add_plugins((MinimalPlugins, TransformPlugin)) + .add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: None, + exit_condition: ExitCondition::DontExit, + ..default() + }), + FrameTimeDiagnosticsPlugin, + LogDiagnosticsPlugin::default(), + )) .add_systems(Startup, setup) // Updating transforms *must* be done before `PostUpdate` // or the hierarchy will momentarily be in an invalid state. diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index aebd3064fff042..2d7dc074dbf047 100644 --- a/examples/tools/scene_viewer/main.rs +++ b/examples/tools/scene_viewer/main.rs @@ -145,6 +145,7 @@ fn setup_scene_after_load( specular_map: asset_server .load("assets/environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), intensity: 150.0, + ..default() }, camera_controller, )); diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index 4b773688c258e8..1e84ca38c3cf10 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -1,7 +1,8 @@ //! This example shows how to align the orientations of objects in 3D space along two axes using the `Transform::align` API. use bevy::color::palettes::basic::{GRAY, RED, WHITE}; -use bevy::input::mouse::{MouseButtonInput, MouseMotion}; +use bevy::input::mouse::{AccumulatedMouseMotion, MouseButtonInput}; +use bevy::math::StableInterpolate; use bevy::prelude::*; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -18,15 +19,9 @@ fn main() { /// This struct stores metadata for a single rotational move of the ship #[derive(Component, Default)] struct Ship { - /// The initial transform of the ship move, the starting point of interpolation - initial_transform: Transform, - /// The target transform of the ship move, the endpoint of interpolation target_transform: Transform, - /// The progress of the ship move in percentage points - progress: u16, - /// Whether the ship is currently in motion; allows motion to be paused in_motion: bool, } @@ -92,7 +87,6 @@ fn setup( ..default() }, Ship { - initial_transform: Transform::IDENTITY, target_transform: random_axes_target_alignment(&RandomAxes(first, second)), ..default() }, @@ -147,37 +141,33 @@ fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) { } // Actually update the ship's transform according to its initial source and target -fn rotate_ship(mut ship: Query<(&mut Ship, &mut Transform)>) { +fn rotate_ship(mut ship: Query<(&mut Ship, &mut Transform)>, time: Res