From 69637b564433cccd5dca64598bde5e0fa29b2bda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 06:56:48 +0000 Subject: [PATCH 001/133] Bump super-linter/super-linter from 8.2.1 to 8.3.0 (#21992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [super-linter/super-linter](https://github.com/super-linter/super-linter) from 8.2.1 to 8.3.0.
Release notes

Sourced from super-linter/super-linter's releases.

v8.3.0

8.3.0 (2025-11-28)

🚀 Features

🐛 Bugfixes

⬆️ Dependency updates

... (truncated)

Changelog

Sourced from super-linter/super-linter's changelog.

8.3.0 (2025-11-28)

🚀 Features

🐛 Bugfixes

⬆️ Dependency updates

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=super-linter/super-linter&package-manager=github_actions&previous-version=8.2.1&new-version=8.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c6e26bc8ef45..6c334489b064a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -304,7 +304,7 @@ jobs: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 - name: Run Markdown Lint - uses: super-linter/super-linter/slim@v8.2.1 + uses: super-linter/super-linter/slim@v8.3.0 env: MULTI_STATUS: false VALIDATE_ALL_CODEBASE: false From 1285ccf7c0548c2605bafabe3ce1dc04d564194f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 06:57:49 +0000 Subject: [PATCH 002/133] Bump crate-ci/typos from 1.39.2 to 1.40.0 (#21993) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.39.2 to 1.40.0.
Release notes

Sourced from crate-ci/typos's releases.

v1.40.0

[1.40.0] - 2025-11-26

Features

Changelog

Sourced from crate-ci/typos's changelog.

[1.40.0] - 2025-11-26

Features

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.39.2&new-version=1.40.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c334489b064a..b687b032e3d13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -336,7 +336,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Check for typos - uses: crate-ci/typos@v1.39.2 + uses: crate-ci/typos@v1.40.0 - name: Typos info if: failure() run: | From c378b7aa7e36a3629fa739ff823d006cb1ebd9cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 07:01:32 +0000 Subject: [PATCH 003/133] Update criterion requirement from 0.7.0 to 0.8.0 (#21994) Updates the requirements on [criterion](https://github.com/criterion-rs/criterion.rs) to permit the latest version.
Changelog

Sourced from criterion's changelog.

0.8.0 - 2025-11-29

BREAKING

  • Drop async-std support

Changed

  • Bump MSRV to 1.86, stable to 1.91.1

Added

  • Add ability to plot throughput on summary page.
  • Add support for reporting throughput in elements and bytes - Throughput::ElementsAndBytes allows the text summary to report throughput in both units simultaneously.
  • Add alloca-based memory layout randomisation to mitigate memory effects on measurements.
  • Add doc comment to benchmark runner in criterion_group macro (removes linter warnings)

Fixed

  • Fix plotting NaN bug

Other

  • Remove Master API Docs links temporarily while we restore the docs publishing.

[0.7.0] - 2025-07-25

  • Bump version of criterion-plot to align dependencies.

[0.6.0] - 2025-05-17

Changed

  • MSRV bumped to 1.80
  • The real_blackbox feature no longer has any impact. Criterion always uses std::hint::black_box() now. Users of criterion::black_box() should switch to std::hint::black_box().
  • clap dependency unpinned.

Fixed

  • gnuplot version is now correctly detected when using certain Windows binaries/configurations that used to fail

Added

  • Async benchmarking with Tokio may be done via a tokio::runtime::Handle, not only a tokio::runtime::Runtime

[0.5.1] - 2023-05-26

Fixed

  • Quick mode (--quick) no longer crashes with measured times over 5 seconds when --noplot is not active

[0.5.0] - 2023-05-23

... (truncated)

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- benches/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/Cargo.toml b/benches/Cargo.toml index f569def4edbfd..8940f1eed7105 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -10,7 +10,7 @@ autobenches = false [dependencies] # The primary crate that runs and analyzes our benchmarks. This is a regular dependency because the # `bench!` macro refers to it in its documentation. -criterion = { version = "0.7.0", features = ["html_reports"] } +criterion = { version = "0.8.0", features = ["html_reports"] } [dev-dependencies] # Bevy crates From 5fbc9f1d3efb6da8bb557c2e8ae97344c97e36ef Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 1 Dec 2025 23:14:21 +0000 Subject: [PATCH 004/133] `PointerbuttonState::clear` (#21997) # Objective To clear the `PointerButtonState`, `clear` is called on each of its individual fields holding the button state data. Instead add a `clear` method to `PointerButtonState`. ## Solution Add a `clear` method to `PointerButtonState` to clear its data. --------- Co-authored-by: Kevin Chen --- crates/bevy_picking/src/events.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 4528001f0e429..4fb10121efde8 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -351,6 +351,15 @@ pub struct PointerButtonState { pub dragging_over: HashMap, } +impl PointerButtonState { + /// Clears all press and drag data tracked for this button on its pointer. + pub fn clear(&mut self) { + self.pressing.clear(); + self.dragging.clear(); + self.dragging_over.clear(); + } +} + /// State for all pointers. #[derive(Debug, Clone, Default, Resource)] pub struct PointerState { @@ -379,9 +388,7 @@ impl PointerState { pub fn clear(&mut self, pointer_id: PointerId) { for button in PointerButton::iter() { if let Some(state) = self.pointer_buttons.get_mut(&(pointer_id, button)) { - state.pressing.clear(); - state.dragging.clear(); - state.dragging_over.clear(); + state.clear(); } } } @@ -699,9 +706,7 @@ pub fn pointer_events( } // Finally, we can clear the state of everything relating to presses or drags. - state.pressing.clear(); - state.dragging.clear(); - state.dragging_over.clear(); + state.clear(); } // Moved PointerAction::Move { delta } => { From 1f97c83bf8d70736c051dbf566da5b901ae90d52 Mon Sep 17 00:00:00 2001 From: Sarthak Singh <35749450+SarthakSingh31@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:35:12 +0530 Subject: [PATCH 005/133] Fix crash on exit due to destroyed surface texture (#21976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fix crash when a window is closed on current main
Crash log ``` ❯ RUST_BACKTRACE=full cargo run --example 3d_scene -F dev Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s Running `target/debug/examples/3d_scene` 2025-11-29T21:39:06.551816Z INFO bevy_diagnostic::system_information_diagnostics_plugin::internal: SystemInfo { os: "Linux (Arch Linux rolling)", kernel: "6.17.9-arch1-1", cpu: "AMD Ryzen 9 9950X 16-Core Processor", core_count: "16", memory: "186.4 GiB" } 2025-11-29T21:39:06.589438Z INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA GeForce RTX 5080", vendor: 4318, device: 11266, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "580.105.08", backend: Vulkan } 2025-11-29T21:39:06.953148Z INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device. 2025-11-29T21:39:06.959263Z INFO bevy_winit::system: Creating new window 3d_scene (0v0) 2025-11-29T21:39:09.503214Z INFO bevy_window::system: No windows are open, exiting 2025-11-29T21:39:09.504687Z INFO bevy_winit::system: Closing window 0v0 2025-11-29T21:39:09.510808Z ERROR wgpu::backend::wgpu_core: Handling wgpu errors as fatal by default thread '' (664835) panicked at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/backend/wgpu_core.rs:1852:22: wgpu error: Validation Error Caused by: In Queue::submit In a pass parameter Texture with '' label has been destroyed stack backtrace: 0: 0x557069cd5e42 - std[cbf56a46dac989f5]::backtrace_rs::backtrace::libunwind::trace at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9 1: 0x557069cd5e42 - std[cbf56a46dac989f5]::backtrace_rs::backtrace::trace_unsynchronized:: at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/../../backtrace/src/backtrace/mod.rs:66:14 2: 0x557069cd5e42 - std[cbf56a46dac989f5]::sys::backtrace::_print_fmt at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/sys/backtrace.rs:68:9 3: 0x557069cd5e42 - <::print::DisplayBacktrace as core[a3424aac13c77cf6]::fmt::Display>::fmt at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/sys/backtrace.rs:38:26 4: 0x557069cf05e7 - ::fmt at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/core/src/fmt/rt.rs:152:76 5: 0x557069cf05e7 - core[a3424aac13c77cf6]::fmt::write 6: 0x557069cdc5d6 - std[cbf56a46dac989f5]::io::default_write_fmt:: at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/io/mod.rs:639:11 7: 0x557069cdc5d6 - ::write_fmt at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/io/mod.rs:1994:13 8: 0x557069cb255a - ::print at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/sys/backtrace.rs:41:9 9: 0x557069cb255a - std[cbf56a46dac989f5]::panicking::default_hook::{closure#0} at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/panicking.rs:292:27 10: 0x557069cce7e1 - std[cbf56a46dac989f5]::panicking::default_hook at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/panicking.rs:319:9 11: 0x5570696cd6b3 - core[a3424aac13c77cf6]::ops::function::Fn<(&'a std[cbf56a46dac989f5]::panic::PanicHookInfo<'b>,), Output = ()> + core[a3424aac13c77cf6]::marker::Send + core[a3424aac13c77cf6]::marker::Sync> as core[a3424aac13c77cf6]::ops::function::Fn<(&std[cbf56a46dac989f5]::panic::PanicHookInfo,)>>::call at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:2220:9 12: 0x5570696b9081 - bevy_ecs[ae55bfe1c74e77a3]::error::bevy_error::bevy_error_panic_hook:: core[a3424aac13c77cf6]::ops::function::Fn<(&'a std[cbf56a46dac989f5]::panic::PanicHookInfo<'b>,), Output = ()> + core[a3424aac13c77cf6]::marker::Send + core[a3424aac13c77cf6]::marker::Sync>>::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/error/bevy_error.rs:162:9 13: 0x557069ccea82 - core[a3424aac13c77cf6]::ops::function::Fn<(&'a std[cbf56a46dac989f5]::panic::PanicHookInfo<'b>,), Output = ()> + core[a3424aac13c77cf6]::marker::Send + core[a3424aac13c77cf6]::marker::Sync> as core[a3424aac13c77cf6]::ops::function::Fn<(&std[cbf56a46dac989f5]::panic::PanicHookInfo,)>>::call at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/alloc/src/boxed.rs:2220:9 14: 0x557069ccea82 - std[cbf56a46dac989f5]::panicking::panic_with_hook at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/panicking.rs:833:13 15: 0x557069cb2618 - std[cbf56a46dac989f5]::panicking::panic_handler::{closure#0} at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/panicking.rs:698:13 16: 0x557069ca9779 - std[cbf56a46dac989f5]::sys::backtrace::__rust_end_short_backtrace:: at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/sys/backtrace.rs:176:18 17: 0x557069cb37cd - __rustc[f3f8441e4970532f]::rust_begin_unwind at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/panicking.rs:689:5 18: 0x557069cf0d5c - core[a3424aac13c77cf6]::panicking::panic_fmt at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/core/src/panicking.rs:80:14 19: 0x5570675ffab5 - wgpu[932fed8fce9d4b4b]::backend::wgpu_core::default_error_handler at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/backend/wgpu_core.rs:659:5 20: 0x5570675ff6e5 - ::handle_error at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/backend/wgpu_core.rs:643:21 21: 0x5570675ff3f4 - ::handle_error_inner at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/backend/wgpu_core.rs:298:14 22: 0x5570675fcda3 - ::handle_error_nolabel:: at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/backend/wgpu_core.rs:323:14 23: 0x55706760ed17 - ::submit at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/backend/wgpu_core.rs:1852:22 24: 0x5570672eef2a - ::submit::> at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wgpu-26.0.1/src/api/queue.rs:253:32 25: 0x557066f4965c - ::run:: at /home/ssingh/Projects/bevy/crates/bevy_render/src/renderer/graph_runner.rs:87:19 26: 0x5570670a05ad - bevy_render[cbe98d17828001b5]::renderer::render_system at /home/ssingh/Projects/bevy/crates/bevy_render/src/renderer/mod.rs:48:15 27: 0x55706744d021 - >)>>::call_mut at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:166:5 28: 0x5570670ad864 - <&mut bevy_render[cbe98d17828001b5]::renderer::render_system as core[a3424aac13c77cf6]::ops::function::FnMut<(&mut bevy_ecs[ae55bfe1c74e77a3]::world::World, &mut bevy_ecs[ae55bfe1c74e77a3]::system::function_system::SystemState>)>>::call_mut at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:298:21 29: 0x55706735dc8e - <_ as bevy_ecs[ae55bfe1c74e77a3]::system::exclusive_function_system::ExclusiveSystemParamFunction _>>::run::call_inner::<(), &mut bevy_ecs[ae55bfe1c74e77a3]::system::function_system::SystemState>, &mut bevy_render[cbe98d17828001b5]::renderer::render_system> at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/system/exclusive_function_system.rs:270:21 30: 0x5570670ad95e - >)>>::run at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/system/exclusive_function_system.rs:273:17 31: 0x55706735e0ab - >), (), bevy_render[cbe98d17828001b5]::renderer::render_system> as bevy_ecs[ae55bfe1c74e77a3]::system::system::System>::run_unsafe::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/system/exclusive_function_system.rs:135:33 32: 0x55706702ac65 - ::last_change_tick_scope::, >), (), bevy_render[cbe98d17828001b5]::renderer::render_system> as bevy_ecs[ae55bfe1c74e77a3]::system::system::System>::run_unsafe::{closure#0}> at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/world/mod.rs:3070:9 33: 0x55706735eb0a - >), (), bevy_render[cbe98d17828001b5]::renderer::render_system> as bevy_ecs[ae55bfe1c74e77a3]::system::system::System>::run_unsafe at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/system/exclusive_function_system.rs:113:15 34: 0x55706735f7ab - >), (), bevy_render[cbe98d17828001b5]::renderer::render_system> as bevy_ecs[ae55bfe1c74e77a3]::system::system::System>::run_without_applying_deferred at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/system/system.rs:138:23 35: 0x55706735f806 - >), (), bevy_render[cbe98d17828001b5]::renderer::render_system> as bevy_ecs[ae55bfe1c74e77a3]::system::system::System>::run at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/system/system.rs:119:24 36: 0x55706988d090 - bevy_ecs[ae55bfe1c74e77a3]::schedule::executor::__rust_begin_short_backtrace::run at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/schedule/executor/mod.rs:288:29 37: 0x5570697be94d - ::spawn_exclusive_system_task::{closure#1}::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs:742:25 38: 0x5570697254f9 - <::spawn_exclusive_system_task::{closure#1}::{closure#0} as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5 39: 0x55706977f259 - ::spawn_exclusive_system_task::{closure#1}::{closure#0}> as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9 40: 0x5570697c3253 - std[cbf56a46dac989f5]::panicking::catch_unwind::do_call::::spawn_exclusive_system_task::{closure#1}::{closure#0}>, ()> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40 41: 0x557069887a9b - __rust_try 42: 0x557069886fae - std[cbf56a46dac989f5]::panicking::catch_unwind::<(), core[a3424aac13c77cf6]::panic::unwind_safe::AssertUnwindSafe<::spawn_exclusive_system_task::{closure#1}::{closure#0}>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19 43: 0x557069886fae - std[cbf56a46dac989f5]::panic::catch_unwind::::spawn_exclusive_system_task::{closure#1}::{closure#0}>, ()> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14 44: 0x5570697bf233 - ::spawn_exclusive_system_task::{closure#1} at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs:740:27 45: 0x55706977f642 - ::spawn_exclusive_system_task::{closure#1}> as core[a3424aac13c77cf6]::future::future::Future>::poll at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:299:9 46: 0x557069765c04 - ::spawn_exclusive_system_task::{closure#1}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0} at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:653:53 47: 0x55706977f4f4 - ::spawn_exclusive_system_task::{closure#1}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}> as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9 48: 0x5570697c374d - std[cbf56a46dac989f5]::panicking::catch_unwind::do_call::::spawn_exclusive_system_task::{closure#1}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}>, core[a3424aac13c77cf6]::task::poll::Poll<()>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40 49: 0x557069887a9b - __rust_try 50: 0x557069887a24 - std[cbf56a46dac989f5]::panicking::catch_unwind::, core[a3424aac13c77cf6]::panic::unwind_safe::AssertUnwindSafe<::spawn_exclusive_system_task::{closure#1}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19 51: 0x557069887a24 - std[cbf56a46dac989f5]::panic::catch_unwind::::spawn_exclusive_system_task::{closure#1}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}>, core[a3424aac13c77cf6]::task::poll::Poll<()>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14 52: 0x557069766ae8 - ::spawn_exclusive_system_task::{closure#1}>> as core[a3424aac13c77cf6]::future::future::Future>::poll at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:653:9 53: 0x557069892db3 - ::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}> as core[a3424aac13c77cf6]::future::future::Future>::poll at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.2/src/lib.rs:1179:31 54: 0x557069896c98 - ::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run::{closure#1} at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/raw.rs:550:21 55: 0x557069725734 - <::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run::{closure#1} as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5 56: 0x55706977f2db - ::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run::{closure#1}> as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9 57: 0x5570697c336e - std[cbf56a46dac989f5]::panicking::catch_unwind::do_call::::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run::{closure#1}>, core[a3424aac13c77cf6]::task::poll::Poll>>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40 58: 0x557069887a9b - __rust_try 59: 0x5570698871c4 - std[cbf56a46dac989f5]::panicking::catch_unwind::>>, core[a3424aac13c77cf6]::panic::unwind_safe::AssertUnwindSafe<::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run::{closure#1}>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19 60: 0x5570698871c4 - std[cbf56a46dac989f5]::panic::catch_unwind::::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run::{closure#1}>, core[a3424aac13c77cf6]::task::poll::Poll>>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14 61: 0x55706989b7cd - ::spawn_exclusive_system_task::{closure#1}>>, ::spawn_inner>, futures_lite[d9f7489fbab33684]::future::CatchUnwind::spawn_exclusive_system_task::{closure#1}>>>::{closure#0}>, core[a3424aac13c77cf6]::result::Result<(), alloc[9fa3fef380221c31]::boxed::Box>, ::schedule::{closure#0}, ()>>::run at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/raw.rs:549:23 62: 0x557069924fd2 - ::run at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/runnable.rs:781:18 63: 0x55706990a9bb - ::tick::{closure#0} at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.2/src/lib.rs:733:18 64: 0x55706990a4b5 - ::tick::{closure#0} at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.2/src/lib.rs:324:29 65: 0x5570697c9dec - ::tick::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/thread_executor.rs:105:39 66: 0x55706975e735 - ::execute_scope::<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:542:45 67: 0x55706977f583 - ::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}> as core[a3424aac13c77cf6]::future::future::Future>::poll at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:299:9 68: 0x557069765b84 - ::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0} at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:653:53 69: 0x55706977f474 - ::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}> as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9 70: 0x5570697c3647 - std[cbf56a46dac989f5]::panicking::catch_unwind::do_call::::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}>, core[a3424aac13c77cf6]::task::poll::Poll> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40 71: 0x557069887a9b - __rust_try 72: 0x5570698877da - std[cbf56a46dac989f5]::panicking::catch_unwind::, core[a3424aac13c77cf6]::panic::unwind_safe::AssertUnwindSafe<::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19 73: 0x5570698877da - std[cbf56a46dac989f5]::panic::catch_unwind::::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}>> as core[a3424aac13c77cf6]::future::future::Future>::poll::{closure#0}>, core[a3424aac13c77cf6]::task::poll::Poll> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14 74: 0x5570697667ea - ::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}::{closure#0}>> as core[a3424aac13c77cf6]::future::future::Future>::poll at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:653:9 75: 0x55706975d6e3 - ::execute_scope::<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:545:77 76: 0x557069766039 - ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}, ::execute_scope<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0}::{closure#0}> as core[a3424aac13c77cf6]::future::future::Future>::poll at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:454:46 77: 0x55706975c15d - ::execute_scope::<(), ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}::{closure#0}>::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:548:41 78: 0x55706975d1e7 - ::scope_with_executor_inner::<::run::{closure#1}, ()>::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:459:85 79: 0x55706976577a - futures_lite[d9f7489fbab33684]::future::block_on::, ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}>::{closure#0} at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:96:35 80: 0x5570698fe6e4 - >>::try_with::, ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}>::{closure#0}, alloc[9fa3fef380221c31]::vec::Vec<()>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:508:12 81: 0x5570698fe563 - >>::with::, ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}>::{closure#0}, alloc[9fa3fef380221c31]::vec::Vec<()>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:472:20 82: 0x5570697654dd - futures_lite[d9f7489fbab33684]::future::block_on::, ::scope_with_executor_inner<::run::{closure#1}, ()>::{closure#0}> at /home/ssingh/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:75:11 83: 0x55706975b709 - ::scope_with_executor_inner::<::run::{closure#1}, ()> at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:413:13 84: 0x55706975c349 - ::scope_with_executor::<::run::{closure#1}, ()>::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:343:22 85: 0x5570698fe407 - >>::try_with::<::scope_with_executor<::run::{closure#1}, ()>::{closure#0}, alloc[9fa3fef380221c31]::vec::Vec<()>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:508:12 86: 0x5570698fe29d - >>::with::<::scope_with_executor<::run::{closure#1}, ()>::{closure#0}, alloc[9fa3fef380221c31]::vec::Vec<()>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:472:20 87: 0x55706975b42c - ::scope_with_executor::<::run::{closure#1}, ()> at /home/ssingh/Projects/bevy/crates/bevy_tasks/src/task_pool.rs:339:31 88: 0x5570698d5d34 - ::run at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs:279:57 89: 0x55706980e792 - ::run at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/schedule/schedule.rs:540:14 90: 0x5570696aca61 - ::run_schedule::>::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/world/mod.rs:3673:57 91: 0x5570696abc76 - ::try_schedule_scope::<(), bevy_ecs[ae55bfe1c74e77a3]::intern::Interned, ::run_schedule>::{closure#0}> at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/world/mod.rs:3591:21 92: 0x5570696abad9 - ::schedule_scope::<(), bevy_ecs[ae55bfe1c74e77a3]::intern::Interned, ::run_schedule>::{closure#0}> at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/world/mod.rs:3643:14 93: 0x5570696abab8 - ::run_schedule::> at /home/ssingh/Projects/bevy/crates/bevy_ecs/src/world/mod.rs:3673:14 94: 0x5570696a8f93 - ::run_default_schedule at /home/ssingh/Projects/bevy/crates/bevy_app/src/sub_app.rs:141:24 95: 0x5570696a91e4 - ::update at /home/ssingh/Projects/bevy/crates/bevy_app/src/sub_app.rs:147:14 96: 0x55706707b40e - ::cleanup::{closure#0} at /home/ssingh/Projects/bevy/crates/bevy_render/src/pipelined_rendering.rs:168:32 97: 0x55706744e516 - std[cbf56a46dac989f5]::sys::backtrace::__rust_begin_short_backtrace::<::cleanup::{closure#0}, ()> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:160:18 98: 0x5570670439c0 - ::spawn_unchecked_::<::cleanup::{closure#0}, ()>::{closure#1}::{closure#0} at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:562:17 99: 0x557066f925c1 - ::spawn_unchecked_<::cleanup::{closure#0}, ()>::{closure#1}::{closure#0}> as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9 100: 0x55706703e4db - std[cbf56a46dac989f5]::panicking::catch_unwind::do_call::::spawn_unchecked_<::cleanup::{closure#0}, ()>::{closure#1}::{closure#0}>, ()> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40 101: 0x557067043a4b - __rust_try 102: 0x5570670435ea - std[cbf56a46dac989f5]::panicking::catch_unwind::<(), core[a3424aac13c77cf6]::panic::unwind_safe::AssertUnwindSafe<::spawn_unchecked_<::cleanup::{closure#0}, ()>::{closure#1}::{closure#0}>> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19 103: 0x5570670435ea - std[cbf56a46dac989f5]::panic::catch_unwind::::spawn_unchecked_<::cleanup::{closure#0}, ()>::{closure#1}::{closure#0}>, ()> at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14 104: 0x5570670435ea - ::spawn_unchecked_::<::cleanup::{closure#0}, ()>::{closure#1} at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/mod.rs:560:30 105: 0x55706742811f - <::spawn_unchecked_<::cleanup::{closure#0}, ()>::{closure#1} as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once::{shim:vtable#0} at /home/ssingh/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5 106: 0x557069cd501f - > as core[a3424aac13c77cf6]::ops::function::FnOnce<()>>::call_once at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/alloc/src/boxed.rs:2206:9 107: 0x557069cd501f - ::new::thread_start at /rustc/cc3eee7fbe17ea4b7238531cb97e1b7b8bd6afce/library/std/src/sys/thread/unix.rs:124:17 108: 0x7f75f1a9698b - 109: 0x7f75f1b1a9cc - 110: 0x0 - Encountered a panic in system `bevy_render::renderer::render_system`! ```
## Solution - Remove the `ViewTarget` when it becomes unavailable ## Testing - Testing by running the bevy examples on linux --------- Co-authored-by: IceSentry --- crates/bevy_render/src/view/mod.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 9a3d610087c63..0226f22c770b8 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -1048,12 +1048,18 @@ pub fn prepare_view_targets( ) { let mut textures = >::default(); for (entity, camera, view, texture_usage, msaa) in cameras.iter() { - let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) - else { - continue; - }; + let (Some(target_size), Some(out_attachment)) = ( + camera.physical_target_size, + camera + .target + .as_ref() + .and_then(|target| view_target_attachments.get(target)), + ) else { + // If we can't find an output attachment we need to remove the ViewTarget + // component to make sure the camera doesn't try rendering to an invalid + // output attachment. + commands.entity(entity).try_remove::(); - let Some(out_attachment) = view_target_attachments.get(target) else { continue; }; From f9d59f82066e86d4cecc16df807e10b33db21879 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Wed, 3 Dec 2025 01:03:54 +0800 Subject: [PATCH 006/133] Fix cargo features lint (#22008) # Objective `markdownlint` fails on `docs/cargo_features.md` as it doesn't like the space before the pipe at the end of this line ## Solution - Add a `.` ## Testing - CI --- Cargo.toml | 6 +++--- docs/cargo_features.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d039936d83779..aa3982b0c3a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -222,7 +222,7 @@ common_api = [ # COLLECTION: Features used to build 2D Bevy apps (does not include a render backend). You generally don't need to worry about this unless you are using a custom renderer. 2d_api = ["common_api", "bevy_sprite"] -# COLLECTION: Bevy's built-in 2D renderer, built on top of `bevy_render` +# COLLECTION: Bevy's built-in 2D renderer, built on top of `bevy_render`. 2d_bevy_render = [ "2d_api", "bevy_render", @@ -245,7 +245,7 @@ common_api = [ "zstd_rust", ] -# COLLECTION: Bevy's built-in 3D renderer, built on top of `bevy_render` +# COLLECTION: Bevy's built-in 3D renderer, built on top of `bevy_render`. 3d_bevy_render = [ "3d_api", "bevy_render", @@ -261,7 +261,7 @@ common_api = [ # COLLECTION: Features used to build UI Bevy apps (does not include a render backend). You generally don't need to worry about this unless you are using a custom renderer. ui_api = ["default_app", "common_api", "bevy_ui"] -# COLLECTION: Bevy's built-in UI renderer, built on top of `bevy_render` +# COLLECTION: Bevy's built-in UI renderer, built on top of `bevy_render`. ui_bevy_render = [ "ui_api", "bevy_render", diff --git a/docs/cargo_features.md b/docs/cargo_features.md index e26c8b51c38af..5f7e33960539d 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -44,11 +44,11 @@ collections to build your own "profile" equivalent, without needing to manually |default_platform|These are platform support features, such as OS support/features, windowing and input backends, etc.| |common_api|Default scene definition features. Note that this does not include an actual renderer, such as bevy_render (Bevy's default render backend).| |2d_api|Features used to build 2D Bevy apps (does not include a render backend). You generally don't need to worry about this unless you are using a custom renderer.| -|2d_bevy_render|Bevy's built-in 2D renderer, built on top of `bevy_render` | +|2d_bevy_render|Bevy's built-in 2D renderer, built on top of `bevy_render`.| |3d_api|Features used to build 3D Bevy apps (does not include a render backend). You generally don't need to worry about this unless you are using a custom renderer.| -|3d_bevy_render|Bevy's built-in 3D renderer, built on top of `bevy_render` | +|3d_bevy_render|Bevy's built-in 3D renderer, built on top of `bevy_render`.| |ui_api|Features used to build UI Bevy apps (does not include a render backend). You generally don't need to worry about this unless you are using a custom renderer.| -|ui_bevy_render|Bevy's built-in UI renderer, built on top of `bevy_render` | +|ui_bevy_render|Bevy's built-in UI renderer, built on top of `bevy_render`.| |default_no_std|Recommended defaults for no_std applications| ### Feature List From 243d5627f26b5e1392cca3a61f8c2138819cf869 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 2 Dec 2025 17:04:24 +0000 Subject: [PATCH 007/133] remove unnecessary clone from `load_font_to_fontdb` (#22009) # Objective `load_font_to_fontdb` clones the font handle needlessly. ## Solution Just call id() on it. --- crates/bevy_text/src/pipeline.rs | 36 +++++++++++++++----------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 0289b78dcdfa9..9fa918ba73ac0 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -620,25 +620,23 @@ pub fn load_font_to_fontdb( map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, Arc)>, fonts: &Assets, ) -> FontFaceInfo { - let font_handle = text_font.font.clone(); - let (face_id, family_name) = map_handle_to_font_id - .entry(font_handle.id()) - .or_insert_with(|| { - let font = fonts.get(font_handle.id()).expect( - "Tried getting a font that was not available, probably due to not being loaded yet", - ); - let data = Arc::clone(&font.data); - let ids = font_system - .db_mut() - .load_font_source(cosmic_text::fontdb::Source::Binary(data)); - - // TODO: it is assumed this is the right font face - let face_id = *ids.last().unwrap(); - let face = font_system.db().face(face_id).unwrap(); - - let family_name = Arc::from(face.families[0].0.as_str()); - (face_id, family_name) - }); + let font_id = text_font.font.id(); + let (face_id, family_name) = map_handle_to_font_id.entry(font_id).or_insert_with(|| { + let font = fonts.get(font_id).expect( + "Tried getting a font that was not available, probably due to not being loaded yet", + ); + let data = Arc::clone(&font.data); + let ids = font_system + .db_mut() + .load_font_source(cosmic_text::fontdb::Source::Binary(data)); + + // TODO: it is assumed this is the right font face + let face_id = *ids.last().unwrap(); + let face = font_system.db().face(face_id).unwrap(); + + let family_name = Arc::from(face.families[0].0.as_str()); + (face_id, family_name) + }); let face = font_system.db().face(*face_id).unwrap(); From 71114e1ddece7e1be430205551c4804ff2b06160 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Tue, 2 Dec 2025 12:33:32 -0500 Subject: [PATCH 008/133] Add debug_group to render graph (#22003) # Objective - Debugging an application with multiple cameras can be really annoying because it's hard to know at a glance which pass is associated with which camera. - wgpu exposes a feature to set debug_group that will add labels around groups of passes and draw commands ## Solution - Expose the debug_group feature to the render graph. - Each sub graph can now specify a debug group - Add a new debug group label to each camera with the camera order and entity id - I also added the new debug_group to the tracy span so you can more easily see which span is associated with which camera ## Testing - I tested the 3d_scene and split_screen examples --- ## Showcase Here's what it looked like before ngfx-ui_cH0BZAQWh9 Here's what the split_screen example looks like inside nvidia nsight with the new debug group labels. You can clearly see each camera clearly identified. ngfx-ui_t4L2AheYS3 --- .../src/render_graph/camera_driver_node.rs | 10 +++++++- .../bevy_render/src/render_graph/context.rs | 3 +++ crates/bevy_render/src/render_graph/node.rs | 2 +- .../bevy_render/src/renderer/graph_runner.rs | 23 ++++++++++++++++--- crates/bevy_ui_render/src/lib.rs | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/crates/bevy_render/src/render_graph/camera_driver_node.rs b/crates/bevy_render/src/render_graph/camera_driver_node.rs index 5a8dc581625ec..4ba4e93d33f0c 100644 --- a/crates/bevy_render/src/render_graph/camera_driver_node.rs +++ b/crates/bevy_render/src/render_graph/camera_driver_node.rs @@ -54,7 +54,15 @@ impl Node for CameraDriverNode { } } if run_graph { - graph.run_sub_graph(camera.render_graph, vec![], Some(sorted_camera.entity))?; + graph.run_sub_graph( + camera.render_graph, + vec![], + Some(sorted_camera.entity), + Some(format!( + "Camera {} ({})", + sorted_camera.order, sorted_camera.entity + )), + )?; } } diff --git a/crates/bevy_render/src/render_graph/context.rs b/crates/bevy_render/src/render_graph/context.rs index 4c6ecd30c1d67..b1f35edc1d1d8 100644 --- a/crates/bevy_render/src/render_graph/context.rs +++ b/crates/bevy_render/src/render_graph/context.rs @@ -14,6 +14,7 @@ pub struct RunSubGraph { pub sub_graph: InternedRenderSubGraph, pub inputs: Vec, pub view_entity: Option, + pub debug_group: Option, } /// The context with all graph information required to run a [`Node`](super::Node). @@ -185,6 +186,7 @@ impl<'a> RenderGraphContext<'a> { name: impl RenderSubGraph, inputs: Vec, view_entity: Option, + debug_group: Option, ) -> Result<(), RunSubGraphError> { let name = name.intern(); let sub_graph = self @@ -219,6 +221,7 @@ impl<'a> RenderGraphContext<'a> { sub_graph: name, inputs, view_entity, + debug_group, }); Ok(()) diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 4355892487c00..c75218c0a2941 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -343,7 +343,7 @@ impl Node for RunGraphOnViewNode { _render_context: &mut RenderContext, _world: &World, ) -> Result<(), NodeRunError> { - graph.run_sub_graph(self.sub_graph, vec![], Some(graph.view_entity()))?; + graph.run_sub_graph(self.sub_graph, vec![], Some(graph.view_entity()), None)?; Ok(()) } } diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index ff2e421982dc7..c1aa664a07bd4 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -76,7 +76,7 @@ impl RenderGraphRunner { } let mut render_context = RenderContext::new(render_device, diagnostics_recorder); - Self::run_graph(graph, None, &mut render_context, world, &[], None)?; + Self::run_graph(graph, None, &mut render_context, world, &[], None, None)?; finalizer(render_context.command_encoder()); let (render_device, mut diagnostics_recorder) = { @@ -108,18 +108,30 @@ impl RenderGraphRunner { world: &'w World, inputs: &[SlotValue], view_entity: Option, + debug_group: Option, ) -> Result<(), RenderGraphRunnerError> { let mut node_outputs: HashMap> = HashMap::default(); #[cfg(feature = "trace")] - let span = if let Some(label) = &sub_graph { - info_span!("run_graph", name = format!("{label:?}")) + let span = if let Some(render_label) = &sub_graph { + let name = format!("{render_label:?}"); + if let Some(debug_group) = debug_group.as_ref() { + info_span!("run_graph", name = name, debug_group = debug_group) + } else { + info_span!("run_graph", name = name) + } } else { info_span!("run_graph", name = "main_graph") }; #[cfg(feature = "trace")] let _guard = span.enter(); + if let Some(debug_group) = debug_group.as_ref() { + render_context + .command_encoder() + .push_debug_group(debug_group); + } + // Queue up nodes without inputs, which can be run immediately let mut node_queue: VecDeque<&NodeState> = graph .iter_nodes() @@ -235,6 +247,7 @@ impl RenderGraphRunner { world, &run_sub_graph.inputs, run_sub_graph.view_entity, + run_sub_graph.debug_group, )?; } } @@ -262,6 +275,10 @@ impl RenderGraphRunner { } } + if debug_group.is_some() { + render_context.command_encoder().pop_debug_group(); + } + Ok(()) } } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 4a292d86ba322..c385bca66d35d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -426,7 +426,7 @@ impl RenderGraphNode for RunUiSubgraphOnUiViewNode { }; // Run the subgraph on the UI view. - graph.run_sub_graph(SubGraphUi, vec![], Some(ui_camera_view.0))?; + graph.run_sub_graph(SubGraphUi, vec![], Some(ui_camera_view.0), None)?; Ok(()) } } From a06de1fb68f3c221b31514e738a00724f13d4d7e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 2 Dec 2025 19:19:37 +0000 Subject: [PATCH 009/133] UI text module refactor (#21630) # Objective The `measure_text_system` and `text_system` systems don't do anything except iterate a query and then delegate to two other functions, `create_text_measure` and `queue_text`, respectively. This doesn't serve any purpose and just makes these systems harder to understand. ## Solution Flatten the module by moving the code from `create_text_measure` and `queue_text` into the systems and remove those functions. --- crates/bevy_ui/src/widget/text.rs | 249 ++++++++++++------------------ 1 file changed, 97 insertions(+), 152 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index d604bc6977cd9..c84b463ddf3cb 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -12,7 +12,7 @@ use bevy_ecs::{ query::With, reflect::ReflectComponent, system::{Query, Res, ResMut}, - world::{Mut, Ref}, + world::Ref, }; use bevy_image::prelude::*; use bevy_math::Vec2; @@ -223,55 +223,6 @@ impl Measure for TextMeasure { } } -#[inline] -fn create_text_measure<'a>( - entity: Entity, - fonts: &Assets, - scale_factor: f64, - spans: impl Iterator, - block: Ref, - text_pipeline: &mut TextPipeline, - mut content_size: Mut, - mut text_flags: Mut, - mut computed: Mut, - font_system: &mut CosmicFontSystem, -) { - match text_pipeline.create_text_measure( - entity, - fonts, - spans, - scale_factor, - &block, - computed.as_mut(), - font_system, - ) { - Ok(measure) => { - if block.linebreak == LineBreak::NoWrap { - content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); - } else { - content_size.set(NodeMeasure::Text(TextMeasure { info: measure })); - } - - // Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute - text_flags.needs_measure_fn = false; - text_flags.needs_recompute = true; - } - Err(TextError::NoSuchFont) => { - // Try again next frame - text_flags.needs_measure_fn = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - }; -} - /// Generates a new [`Measure`] for a text node on changes to its [`Text`] component. /// /// A `Measure` is used by the UI's layout algorithm to determine the appropriate amount of space @@ -300,98 +251,61 @@ pub fn measure_text_system( mut text_pipeline: ResMut, mut font_system: ResMut, ) { - for (entity, block, content_size, text_flags, computed, computed_target, computed_node) in - &mut text_query + for ( + entity, + block, + mut content_size, + mut text_flags, + mut computed, + computed_target, + computed_node, + ) in &mut text_query { // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). // 1e-5 epsilon to ignore tiny scale factor float errors - if 1e-5 + if !(1e-5 < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() || computed.needs_rerender() || text_flags.needs_measure_fn - || content_size.is_added() + || content_size.is_added()) { - create_text_measure( - entity, - &fonts, - computed_target.scale_factor.into(), - text_reader.iter(entity), - block, - &mut text_pipeline, - content_size, - text_flags, - computed, - &mut font_system, - ); + continue; } - } -} - -#[inline] -fn queue_text( - entity: Entity, - fonts: &Assets, - text_pipeline: &mut TextPipeline, - font_atlas_set: &mut FontAtlasSet, - texture_atlases: &mut Assets, - textures: &mut Assets, - scale_factor: f32, - inverse_scale_factor: f32, - block: &TextLayout, - node: Ref, - mut text_flags: Mut, - text_layout_info: Mut, - computed: &mut ComputedTextBlock, - text_reader: &mut TextUiReader, - font_system: &mut CosmicFontSystem, - swash_cache: &mut SwashCache, -) { - // Skip the text node if it is waiting for a new measure func - if text_flags.needs_measure_fn { - return; - } - let physical_node_size = if block.linebreak == LineBreak::NoWrap { - // With `NoWrap` set, no constraints are placed on the width of the text. - TextBounds::UNBOUNDED - } else { - // `scale_factor` is already multiplied by `UiScale` - TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) - }; + match text_pipeline.create_text_measure( + entity, + fonts.as_ref(), + text_reader.iter(entity), + computed_target.scale_factor.into(), + &block, + computed.as_mut(), + &mut font_system, + ) { + Ok(measure) => { + if block.linebreak == LineBreak::NoWrap { + content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); + } else { + content_size.set(NodeMeasure::Text(TextMeasure { info: measure })); + } - let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.queue_text( - text_layout_info, - fonts, - text_reader.iter(entity), - scale_factor.into(), - block, - physical_node_size, - font_atlas_set, - texture_atlases, - textures, - computed, - font_system, - swash_cache, - ) { - Err(TextError::NoSuchFont) => { - // There was an error processing the text layout, try again next frame - text_flags.needs_recompute = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = scale_factor; - text_layout_info.size *= inverse_scale_factor; - text_flags.needs_recompute = false; - } + // Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute + text_flags.needs_measure_fn = false; + text_flags.needs_recompute = true; + } + Err(TextError::NoSuchFont) => { + // Try again next frame + text_flags.needs_measure_fn = true; + } + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToGetGlyphImage(_) + | TextError::MissingAtlasLayout + | TextError::MissingAtlasTexture + | TextError::InconsistentAtlasState), + ) => { + panic!("Fatal error when processing text: {e}."); + } + }; } } @@ -421,26 +335,57 @@ pub fn text_system( mut font_system: ResMut, mut swash_cache: ResMut, ) { - for (entity, node, block, text_layout_info, text_flags, mut computed) in &mut text_query { - if node.is_changed() || text_flags.needs_recompute { - queue_text( - entity, - &fonts, - &mut text_pipeline, - &mut font_atlas_set, - &mut texture_atlases, - &mut textures, - node.inverse_scale_factor.recip(), - node.inverse_scale_factor, - block, - node, - text_flags, - text_layout_info, - computed.as_mut(), - &mut text_reader, - &mut font_system, - &mut swash_cache, - ); + for (entity, node, block, text_layout_info, mut text_flags, mut computed) in &mut text_query { + // Skip the text node if it is waiting for a new measure func + if text_flags.needs_measure_fn { + continue; + } + + if !(node.is_changed() || text_flags.needs_recompute) { + continue; + } + + let physical_node_size = if block.linebreak == LineBreak::NoWrap { + // With `NoWrap` set, no constraints are placed on the width of the text. + TextBounds::UNBOUNDED + } else { + // `scale_factor` is already multiplied by `UiScale` + TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) + }; + + let text_layout_info = text_layout_info.into_inner(); + match text_pipeline.queue_text( + text_layout_info, + &fonts, + text_reader.iter(entity), + node.inverse_scale_factor.recip() as f64, + block, + physical_node_size, + &mut font_atlas_set, + &mut texture_atlases, + &mut textures, + &mut computed, + &mut font_system, + &mut swash_cache, + ) { + Err(TextError::NoSuchFont) => { + // There was an error processing the text layout, try again next frame + text_flags.needs_recompute = true; + } + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToGetGlyphImage(_) + | TextError::MissingAtlasLayout + | TextError::MissingAtlasTexture + | TextError::InconsistentAtlasState), + ) => { + panic!("Fatal error when processing text: {e}."); + } + Ok(()) => { + text_layout_info.scale_factor = node.inverse_scale_factor.recip(); + text_layout_info.size *= node.inverse_scale_factor; + text_flags.needs_recompute = false; + } } } } From 8c78e46987aaa0036219ebc996ba0f1ad13dbe57 Mon Sep 17 00:00:00 2001 From: HeartofPhos <43216176+HeartofPhos@users.noreply.github.com> Date: Wed, 3 Dec 2025 02:35:59 +0200 Subject: [PATCH 010/133] #21980 Fix Component/Relationship derive using non fully qualified path (#21981) # Objective Fixes #21980 ## Solution Use fully qualified path for `Entity` ## Testing The following code now compiles ```rust mod test { fn derive_component_relationship_hygiene() { #[derive(Debug, bevy::prelude::Component)] #[relationship(relationship_target = RelTarget)] struct Rel(pub bevy::prelude::Entity); #[derive(Debug, bevy::prelude::Component)] #[relationship_target(relationship = Rel)] struct RelTarget(bevy::prelude::Entity); } }``` Co-authored-by: Heart <> --- crates/bevy_ecs/macros/src/component.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 5ec637da1a4f7..f37defab0b111 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -826,7 +826,7 @@ fn derive_relationship( } #[inline] - fn set_risky(&mut self, entity: Entity) { + fn set_risky(&mut self, entity: #bevy_ecs_path::entity::Entity) { self.#relationship_member = entity; } } From 8978b479912e3bba0aabd8939f7a4339b14157b9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 3 Dec 2025 01:04:12 +0000 Subject: [PATCH 011/133] Fix dragging-over panic (#21999) # Objective Problems: * If a drag starts over a hovered entity, `picking_events` doesn't add the hovered entity to the `dragging_over` map in `PointerButtonState`. This causes a panic when the pointer is moved and `picking_events` tries to update the non-existant `dragging_over` state for the hovered entity. * `DragEnter` isn't dispatched on `DragStart`. This doesn't seem ideal. The drag isn't crossing a geometric boundary, but it is causing the dragged over entities to enter into a dragged over state. Fixes #21998 ## Solution * Update the `dragging_state` for all hovered entities every frame, before handling any `PointerInput`s. * Reworked the `DragEnter` dispatch logic. `DragEnter` now also fires when a drag starts over an already hovered entity. ## Testing The examples using drag events should no longer panic: ``` cargo run --example standard_widgets --features="experimental_bevy_ui_widgets" ``` ``` cargo run --example ui_drag_and_drop ```` --------- Co-authored-by: Kevin Chen --- crates/bevy_picking/src/events.rs | 75 +++++++++++++------ .../dragenter-now-fires-on-drag-starts.md | 6 ++ 2 files changed, 59 insertions(+), 22 deletions(-) create mode 100644 release-content/migration-guides/dragenter-now-fires-on-drag-starts.md diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 4fb10121efde8..1385961e468d5 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -255,7 +255,7 @@ pub struct DragEnd { pub distance: Vec2, } -/// Fires when a pointer dragging the `dragged` entity enters the [target entity](EntityEvent::event_target). +/// Fires when a pointer dragging the `dragged` entity enters the [target entity](EntityEvent::event_target) #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragEnter { @@ -535,31 +535,33 @@ pub fn pointer_events( } } - // If the entity is hovered... + // Iterate all currently hovered entities for each pointer 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; - }; - - // Possibly send DragEnter events - for button in PointerButton::iter() { - let state = pointer_state.get_mut(pointer_id, button); + // Continue if the pointer does not have a valid location. + let Some(location) = pointer_location(pointer_id) else { + debug!( + "Unable to get location for pointer {:?} during pointer over", + pointer_id + ); + continue; + }; + // For each button update its `dragging_over` state and possibly emit DragEnter events. + for button in PointerButton::iter() { + let state = pointer_state.get_mut(pointer_id, button); + + // Only update the `dragging_over` state if there is at least one entity being dragged. + // Only emit DragEnter events for this `hovered_entity`, if it had no previous `dragging_over` state. + if !state.dragging.is_empty() + && state + .dragging_over + .insert(hovered_entity, hit.clone()) + .is_none() + { for drag_target in state.dragging.keys() { - state.dragging_over.insert(hovered_entity, hit.clone()); let drag_enter_event = Pointer::new( pointer_id, location.clone(), @@ -574,8 +576,14 @@ pub fn pointer_events( message_writers.drag_enter_events.write(drag_enter_event); } } + } - // Always send Over events + // Emit an Over event if the `hovered_entity` was not hovered by the same pointer the previous frame. + if !previous_hover_map + .get(&pointer_id) + .iter() + .any(|e| e.contains_key(&hovered_entity)) + { let over_event = Pointer::new( pointer_id, location.clone(), @@ -738,8 +746,32 @@ pub fn pointer_events( }, *press_target, ); + commands.trigger(drag_start_event.clone()); message_writers.drag_start_events.write(drag_start_event); + + // Insert dragging over state and emit DragEnter for hovered entities. + for (hovered_entity, hit) in hover_map + .get(&pointer_id) + .iter() + .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) + .filter(|(hovered_entity, _)| *hovered_entity != *press_target) + { + // Inserting the `dragging_over` state here ensures the `DragEnter` event won't be dispatched twice. + state.dragging_over.insert(hovered_entity, hit.clone()); + let drag_enter_event = Pointer::new( + pointer_id, + location.clone(), + DragEnter { + button, + dragged: *press_target, + hit: hit.clone(), + }, + hovered_entity, + ); + commands.trigger(drag_enter_event.clone()); + message_writers.drag_enter_events.write(drag_enter_event); + } } // Emit Drag events to the entities we are dragging @@ -771,7 +803,6 @@ pub fn pointer_events( .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) .filter(|(hovered_entity, _)| *hovered_entity != *drag_target) { - *state.dragging_over.get_mut(&hovered_entity).unwrap() = hit.clone(); let drag_over_event = Pointer::new( pointer_id, location.clone(), diff --git a/release-content/migration-guides/dragenter-now-fires-on-drag-starts.md b/release-content/migration-guides/dragenter-now-fires-on-drag-starts.md new file mode 100644 index 0000000000000..187067a05cb82 --- /dev/null +++ b/release-content/migration-guides/dragenter-now-fires-on-drag-starts.md @@ -0,0 +1,6 @@ +--- +title: "DragEnter now fires on drag starts" +pull_requests: [21999] +--- + +`DragEnter` now also fires when a drag starts over an already hovered entity. From aa14ada6d45225b0123b4799e7baf2722edabc6d Mon Sep 17 00:00:00 2001 From: Nico Zweifel <34443492+NicoZweifel@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:00:48 +0100 Subject: [PATCH 012/133] Fix extended_material_bindless example wgpu validation error on MacOS/Metal (#21729) # Objective - Fixes https://github.com/bevyengine/bevy/issues/21719 ## Solution - Implementing `bindless_supported` of the `AsBindGroup` trait for `ExtendedMaterial`: ```rust fn bindless_supported(render_device: &RenderDevice) -> bool { B::bindless_supported(render_device) && E::bindless_supported(render_device) } ``` Before it just returned the default (true), leading to the mismatch. I made small, mostly stylistic, improvements to `fn bind_group_layout_entries` and a few other locations like a comment in `extended_material` during debugging but I could move that out into a separate PR as well. Since the changes are rather limited and related to the issue I decided to leave it in for now. ## Testing - Tested on Windows/NixOS and MacOS, tested multiple examples but most testing happened on the `extended_material_bindless` and `extended_material` examples. --- crates/bevy_pbr/src/extended_material.rs | 31 ++++++++++++++---------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 258408cf4cc62..535128d00ec1e 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -196,6 +196,10 @@ impl AsBindGroup for ExtendedMaterial { } } + fn bindless_supported(render_device: &RenderDevice) -> bool { + B::bindless_supported(render_device) && E::bindless_supported(render_device) + } + fn label() -> &'static str { E::label() } @@ -216,7 +220,7 @@ impl AsBindGroup for ExtendedMaterial { ) -> Result { force_non_bindless = force_non_bindless || Self::bindless_slot_count().is_none(); - // add together the bindings of the base material and the user material + // add together the bindings of the base material and the extension let UnpreparedBindGroup { mut bindings } = B::unprepared_bind_group( &self.base, layout, @@ -224,7 +228,9 @@ impl AsBindGroup for ExtendedMaterial { base_param, force_non_bindless, )?; - let extended_bindgroup = E::unprepared_bind_group( + let UnpreparedBindGroup { + bindings: extension_bindings, + } = E::unprepared_bind_group( &self.extension, layout, render_device, @@ -232,7 +238,7 @@ impl AsBindGroup for ExtendedMaterial { force_non_bindless, )?; - bindings.extend(extended_bindgroup.bindings.0); + bindings.extend(extension_bindings.0); Ok(UnpreparedBindGroup { bindings }) } @@ -251,17 +257,16 @@ impl AsBindGroup for ExtendedMaterial { // when bindless mode is on, because of the common bindless resource // arrays, and we need to eliminate the duplicates or `wgpu` will // complain. - let mut entries = vec![]; - let mut seen_bindings = HashSet::<_>::with_hasher(FixedHasher); - for entry in B::bind_group_layout_entries(render_device, force_non_bindless) + let base_entries = B::bind_group_layout_entries(render_device, force_non_bindless); + let extension_entries = E::bind_group_layout_entries(render_device, force_non_bindless); + + let mut seen_bindings = HashSet::::with_hasher(FixedHasher); + + base_entries .into_iter() - .chain(E::bind_group_layout_entries(render_device, force_non_bindless).into_iter()) - { - if seen_bindings.insert(entry.binding) { - entries.push(entry); - } - } - entries + .chain(extension_entries) + .filter(|entry| seen_bindings.insert(entry.binding)) + .collect() } fn bindless_descriptor() -> Option { From 41f59f1891067799eac93796b3e1ffb9775002b5 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Wed, 3 Dec 2025 12:16:24 -0800 Subject: [PATCH 013/133] TypeData dependencies (#22016) # Objective The proposed ["Resources as entities"](https://github.com/bevyengine/bevy/pull/20934 ) PR makes Resource implement Component. This creates a situation where we need to do: ```rust #[derive(Resource, Reflect)] #[reflect(Resource, Component)] struct Thing; ``` I'm of the mind that we should add the ability for TypeData to have "dependencies". Ex `reflect(Resource)` implies `reflect(Component)`. For "subtrait" types, I think supporting this is logical / users would appreciate it. ## Solution 1. Add a new `FromType::insert_dependencies` function with a default impl (which makes this a non-breaking change). This does kind of overload the `FromType` trait (ex: a name like `GetTypeData` might be better with this new context), but this is a pretty niche trait / piece of functionality, and I like the idea of not breaking people. 2. Add a new `TypeRegistration::register_type_data` function, which initializes the TypeData `T` for a given type `V` , inserts that type data, and also inserts any dependent type data using `insert_dependencies`. 3. Adjust the `Reflect` macro to use `register_type_data` instead of `insert(FromType::::from_type())` This makes it possible to do the following: ```rust impl FromType for ReflectResource { fn from_type() -> Self { ReflectResource } fn insert_dependencies(type_registration: &mut TypeRegistration) { type_registration.register_type_data::(); } } ``` Which then allows dropping `reflect(Component)`: ```rust #[derive(Resource, Reflect)] #[reflect(Resource)] struct Thing; ``` ## Testing I added a unit test :stuck_out_tongue_winking_eye: --- .../bevy_reflect/derive/src/registration.rs | 2 +- crates/bevy_reflect/src/lib.rs | 30 +++++++++++++++++++ crates/bevy_reflect/src/type_registry.rs | 11 +++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/bevy_reflect/derive/src/registration.rs b/crates/bevy_reflect/derive/src/registration.rs index 2d8174cfb6095..d3ec6e0ad68a7 100644 --- a/crates/bevy_reflect/derive/src/registration.rs +++ b/crates/bevy_reflect/derive/src/registration.rs @@ -49,7 +49,7 @@ pub(crate) fn impl_get_type_registration<'a>( registration.insert::<#bevy_reflect_path::ReflectFromPtr>(#bevy_reflect_path::FromType::::from_type()); #from_reflect_data #serialization_data - #(registration.insert::<#registration_data>(#bevy_reflect_path::FromType::::from_type());)* + #(registration.register_type_data::<#registration_data, Self>();)* registration } diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 72397bfe39674..7ead2e662e3f9 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -3606,6 +3606,36 @@ bevy_reflect::tests::Test { assert!(registry.contains(TypeId::of::())); assert!(registry.contains(TypeId::of::())); } + + #[test] + fn type_data_dependency() { + #[derive(Reflect)] + #[reflect(A)] + struct X; + + #[derive(Clone)] + struct ReflectA; + + impl FromType for ReflectA { + fn from_type() -> Self { + ReflectA + } + + fn insert_dependencies(type_registration: &mut TypeRegistration) { + type_registration.insert(ReflectB); + } + } + + #[derive(Clone)] + struct ReflectB; + + let mut registry = TypeRegistry::new(); + registry.register::(); + + let registration = registry.get(TypeId::of::()).unwrap(); + assert!(registration.data::().is_some()); + assert!(registration.data::().is_some()); + } } #[cfg(feature = "glam")] diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 5c79a7bd77840..bf1950b7af9cb 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -588,6 +588,13 @@ impl TypeRegistration { self.data.insert(TypeId::of::(), Box::new(data)); } + /// Inserts the [`TypeData`] instance of `T` created for `V`, and inserts any + /// [`TypeData`] dependencies for that combination of `T` and `V`. + pub fn register_type_data, V>(&mut self) { + self.insert(T::from_type()); + T::insert_dependencies(self); + } + /// Returns a reference to the value of type `T` in this registration's /// [type data]. /// @@ -747,6 +754,10 @@ where pub trait FromType { /// Creates an instance of `Self` for type `T`. fn from_type() -> Self; + /// Inserts [`TypeData`] dependencies of this [`TypeData`]. + /// This is especially useful for trait [`TypeData`] that has a supertrait (ex: `A: B`). + /// When the [`TypeData`] for `A` is inserted, the `B` [`TypeData`] will also be inserted. + fn insert_dependencies(_type_registration: &mut TypeRegistration) {} } /// A struct used to serialize reflected instances of a type. From 540d5853dae31006b3e2bef057c3c6299d1dba6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Wed, 3 Dec 2025 14:29:39 -0800 Subject: [PATCH 014/133] Add Clone derive to ViewTarget (#22024) For esoteric reasons we need to send this to the MainWorld for processing. All the fields are GPU resources or Arc'd so it can be trivially cloned. --- crates/bevy_render/src/view/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 0226f22c770b8..040d0ce7bc3cc 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -601,7 +601,7 @@ pub struct ViewUniformOffset { pub offset: u32, } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ViewTarget { main_textures: MainTargetTextures, main_texture_format: TextureFormat, From 1720e5b71b29a8b508ec0ab9f297a42c7ed16de2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 4 Dec 2025 02:35:26 +0000 Subject: [PATCH 015/133] Ignore text buffer metrics (#22022) # Objective Since we set explicit font size and line height attributes for every text section, the `Metrics` values passed to the text buffer don't matter. We can just initialize the buffer with some dummy value and then ignore it. ## Solution * Initialize the text buffer to some dummy values. * Remove the code that collects and sets the metrics during text updates. --- crates/bevy_text/src/pipeline.rs | 15 +-------------- crates/bevy_text/src/text.rs | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 9fa918ba73ac0..05f916f086462 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -105,8 +105,6 @@ impl TextPipeline { // Collect span information into a vec. This is necessary because font loading requires mut access // to FontSystem, which the cosmic-text Buffer also needs. - let mut max_font_size: f32 = 0.; - let mut max_line_height: f32 = 0.0; let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color, LineHeight)> = core::mem::take(&mut self.spans_buffer) .into_iter() @@ -147,10 +145,6 @@ impl TextPipeline { return Err(TextError::NoSuchFont); } - // Get max font size for use in cosmic Metrics. - max_font_size = max_font_size.max(text_font.font_size); - max_line_height = max_line_height.max(line_height.eval(text_font.font_size)); - // Load Bevy fonts into cosmic-text's font system. let face_info = load_font_to_fontdb( text_font, @@ -170,13 +164,6 @@ impl TextPipeline { spans.push((span_index, span, text_font, face_info, color, line_height)); } - let mut metrics = Metrics::new(max_font_size, max_line_height).scale(scale_factor as f32); - // Metrics of 0.0 cause `Buffer::set_metrics` to panic. We hack around this by 'falling - // through' to call `Buffer::set_rich_text` with zero spans so any cached text will be cleared without - // deallocating the buffer. - metrics.font_size = metrics.font_size.max(0.000001); - metrics.line_height = metrics.line_height.max(0.000001); - // Map text sections to cosmic-text spans, and ignore sections with negative or zero fontsizes, // since they cannot be rendered by cosmic-text. // @@ -201,7 +188,7 @@ impl TextPipeline { // Update the buffer. let buffer = &mut computed.buffer; - buffer.set_metrics_and_size(font_system, metrics, bounds.width, bounds.height); + buffer.set_size(font_system, bounds.width, bounds.height); buffer.set_wrap( font_system, diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index f100eca0c786b..6b022487bca6e 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -18,7 +18,7 @@ pub struct CosmicBuffer(pub Buffer); impl Default for CosmicBuffer { fn default() -> Self { - Self(Buffer::new_empty(Metrics::new(0.0, 0.000001))) + Self(Buffer::new_empty(Metrics::new(20.0, 20.0))) } } From 6cbc4b242096e9ef6cf6fc3bd07a3e06d9b916a6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 4 Dec 2025 02:38:40 +0000 Subject: [PATCH 016/133] Remove redundant `shape_until_scroll` (#22020) # Objective In `TextPipeline::update_buffer` the `shape_until_scroll` call is redundant. The preceding line that calls `set_rich_text`, calls `shape_until_scroll` itself before returning. ## Solution Remove the `shape_until_scroll` call. ## Testing The `testbed_ui` screenshot CI will fail with no text rendered if this is incorrect. --- crates/bevy_text/src/pipeline.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 05f916f086462..5d3fe166d1f82 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -208,8 +208,6 @@ impl TextPipeline { Some(justify.into()), ); - buffer.shape_until_scroll(font_system, false); - // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 if bounds.width.is_none() && justify != Justify::Left { From e8520c1faf5f0a1e164947bcfebf275f1c7e7942 Mon Sep 17 00:00:00 2001 From: Sigma-dev <72008323+Sigma-dev@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:19:30 +0100 Subject: [PATCH 017/133] Added required features data to docs export (#21975) # Objective This PR is a sister PR necessary for the website to have `required_features` data. You will find more information on the motivations there: https://github.com/bevyengine/bevy-website/pull/2313 ## Solution - Add the required_features data to the examples docs export ## Testing I tested both together and it worked fine --- tools/example-showcase/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/example-showcase/src/main.rs b/tools/example-showcase/src/main.rs index a437533674fe1..dbc60564623bf 100644 --- a/tools/example-showcase/src/main.rs +++ b/tools/example-showcase/src/main.rs @@ -596,6 +596,7 @@ code_path = \"content/examples{}/{}\" shader_code_paths = {:?} github_code_path = \"{}\" header_message = \"Examples ({})\" +required_features = {:?} +++ {} @@ -637,6 +638,7 @@ header_message = \"Examples ({})\" WebApi::Webgpu => "WebGPU", WebApi::Webgl2 => "WebGL2", }, + to_show.required_features, docblock, ) .as_bytes(), From 6e88ae2db28e5b365c7309f00efb871f210758cb Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Thu, 4 Dec 2025 18:31:59 -0800 Subject: [PATCH 018/133] Inline register_type_data to cut down on binary size (#22030) # Objective My [TypeData dependencies](#22016 ) PR regressed native binary size by ~8mb. (thank you [twitcher](https://github.com/bevyengine/twitcher/)) image ## Solution - Inline `register_type_data`, which restores the original binary size: - `release breakout` before #22016: 127.3mb - `release breakout` after #22016: 136.4mb - `release breakout` after this PR: 127.4mb --- crates/bevy_reflect/src/type_registry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index bf1950b7af9cb..8ccd81ce710f7 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -590,6 +590,7 @@ impl TypeRegistration { /// Inserts the [`TypeData`] instance of `T` created for `V`, and inserts any /// [`TypeData`] dependencies for that combination of `T` and `V`. + #[inline] pub fn register_type_data, V>(&mut self) { self.insert(T::from_type()); T::insert_dependencies(self); From 700ee46292c68dfb8251e43690bbce56fe12f79b Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 5 Dec 2025 00:01:33 -0800 Subject: [PATCH 019/133] Fix some lints from rust 1.89 bump (#22034) # Objective - In #21822 the MSRV bump triggers some new lint warnings, so this pr fixes them. ## Solution - Bumped the msrv in bevy_ecs and fixed the lints. But this pr does not actually bump the msrv. These fixes are all for combining nested if's together. --- crates/bevy_ecs/src/component/clone.rs | 20 +++++----- crates/bevy_ecs/src/entity/clone_entities.rs | 10 ++--- crates/bevy_ecs/src/observer/mod.rs | 24 ++++++------ crates/bevy_ecs/src/query/fetch.rs | 8 ++-- crates/bevy_ecs/src/relationship/mod.rs | 41 ++++++++++---------- crates/bevy_ecs/src/system/system.rs | 8 ++-- 6 files changed, 54 insertions(+), 57 deletions(-) diff --git a/crates/bevy_ecs/src/component/clone.rs b/crates/bevy_ecs/src/component/clone.rs index 3a3784350e36a..a645463923bb6 100644 --- a/crates/bevy_ecs/src/component/clone.rs +++ b/crates/bevy_ecs/src/component/clone.rs @@ -110,20 +110,18 @@ pub fn component_clone_via_reflect(source: &SourceComponent, ctx: &mut Component // Try to clone using ReflectFromReflect if let Some(reflect_from_reflect) = registry.get_type_data::(type_id) - { - if let Some(mut component) = + && let Some(mut component) = reflect_from_reflect.from_reflect(source_component_reflect.as_partial_reflect()) + { + if let Some(reflect_component) = + registry.get_type_data::(type_id) { - if let Some(reflect_component) = - registry.get_type_data::(type_id) - { - reflect_component.map_entities(&mut *component, ctx.entity_mapper()); - } - drop(registry); - - ctx.write_target_component_reflect(component); - return; + reflect_component.map_entities(&mut *component, ctx.entity_mapper()); } + drop(registry); + + ctx.write_target_component_reflect(component); + return; } // Else, try to clone using ReflectDefault if let Some(reflect_default) = diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index ac0b8f3894ee6..0f7105e0eb0ce 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1144,11 +1144,11 @@ impl OptOut { #[inline] fn filter_deny(&mut self, id: ComponentId, world: &World) { self.deny.insert(id); - if self.attach_required_by_components { - if let Some(required_by) = world.components().get_required_by(id) { - self.deny.extend(required_by.iter()); - }; - } + if self.attach_required_by_components + && let Some(required_by) = world.components().get_required_by(id) + { + self.deny.extend(required_by.iter()); + }; } } diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 7ba90f68e06b2..e10ec4c5e0ddd 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -219,18 +219,18 @@ impl World { && observers.entity_component_observers.is_empty() { cache.component_observers.remove(component); - if let Some(flag) = Observers::is_archetype_cached(event_key) { - if let Some(by_component) = archetypes.by_component.get(component) { - for archetype in by_component.keys() { - let archetype = &mut archetypes.archetypes[archetype.index()]; - if archetype.contains(*component) { - let no_longer_observed = archetype - .iter_components() - .all(|id| !cache.component_observers.contains_key(&id)); - - if no_longer_observed { - archetype.flags.set(flag, false); - } + if let Some(flag) = Observers::is_archetype_cached(event_key) + && let Some(by_component) = archetypes.by_component.get(component) + { + for archetype in by_component.keys() { + let archetype = &mut archetypes.archetypes[archetype.index()]; + if archetype.contains(*component) { + let no_longer_observed = archetype + .iter_components() + .all(|id| !cache.component_observers.contains_key(&id)); + + if no_longer_observed { + archetype.flags.set(flag, false); } } } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index d672b6ae4d0b7..2cab429557c7c 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -3319,10 +3319,10 @@ mod tests { fn system(query: Query) { for entity_ref in &query { - if let Some(c) = entity_ref.get_ref::() { - if !c.is_added() { - panic!("Expected C to be added"); - } + if let Some(c) = entity_ref.get_ref::() + && !c.is_added() + { + panic!("Expected C to be added"); } } } diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 2778568dead6e..62849f4977675 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -188,28 +188,27 @@ pub trait Relationship: Component + Sized { } } let target_entity = world.entity(entity).get::().unwrap().get(); - if let Ok(mut target_entity_mut) = world.get_entity_mut(target_entity) { - if let Some(mut relationship_target) = + if let Ok(mut target_entity_mut) = world.get_entity_mut(target_entity) + && let Some(mut relationship_target) = target_entity_mut.get_mut::() - { - relationship_target.collection_mut_risky().remove(entity); - if relationship_target.len() == 0 { - let command = |mut entity: EntityWorldMut| { - // this "remove" operation must check emptiness because in the event that an identical - // relationship is inserted on top, this despawn would result in the removal of that identical - // relationship ... not what we want! - if entity - .get::() - .is_some_and(RelationshipTarget::is_empty) - { - entity.remove::(); - } - }; - - world - .commands() - .queue_silenced(command.with_entity(target_entity)); - } + { + relationship_target.collection_mut_risky().remove(entity); + if relationship_target.len() == 0 { + let command = |mut entity: EntityWorldMut| { + // this "remove" operation must check emptiness because in the event that an identical + // relationship is inserted on top, this despawn would result in the removal of that identical + // relationship ... not what we want! + if entity + .get::() + .is_some_and(RelationshipTarget::is_empty) + { + entity.remove::(); + } + }; + + world + .commands() + .queue_silenced(command.with_entity(target_entity)); } } } diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index a69a1355a66e1..234af6bdf2fee 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -442,10 +442,10 @@ where // Note that the `downcast_mut` check is based on the static type, // and can be optimized out after monomorphization. let any: &mut dyn Any = &mut value; - if let Some(err) = any.downcast_mut::() { - if err.skipped { - return Self::Skipped(core::mem::replace(err, SystemParamValidationError::EMPTY)); - } + if let Some(err) = any.downcast_mut::() + && err.skipped + { + return Self::Skipped(core::mem::replace(err, SystemParamValidationError::EMPTY)); } Self::Failed(From::from(value)) } From ecbb5626d41321f067c03b84cce68c4333ee89ea Mon Sep 17 00:00:00 2001 From: bilsen <40690317+bilsen@users.noreply.github.com> Date: Fri, 5 Dec 2025 19:14:55 +0100 Subject: [PATCH 020/133] Clarify docs for `ExtractSchedule` (#22012) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous docs incorrectly states that the `ExtractSchedule` is run on the main world. Also some small additional changes. --------- Co-authored-by: Ivar Källström Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> --- crates/bevy_render/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 11b6ceb63bce4..b1e7837b3ec04 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -247,13 +247,13 @@ impl Render { } } -/// Schedule which extract data from the main world and inserts it into the render world. +/// Schedule in which data from the main world is 'extracted' into the render world. /// /// This step should be kept as short as possible to increase the "pipelining potential" for /// running the next frame while rendering the current frame. /// -/// This schedule is run on the main world, but its buffers are not applied -/// until it is returned to the render world. +/// This schedule is run on the render world, but it also has access to the main world. +/// See [`MainWorld`] and [`Extract`] for details on how to access main world data from this schedule. #[derive(ScheduleLabel, PartialEq, Eq, Debug, Clone, Hash, Default)] pub struct ExtractSchedule; From dba7c749649c257640489a7c2503d75aec197fd3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 6 Dec 2025 17:10:14 +0000 Subject: [PATCH 021/133] Don't update the text buffer in `text_system` (#21966) # Objective Text is shaped in `measure_text_system`. When the schedule reaches `text_system` there's no need to reupdate the cosmic-text buffer a second time. `text_system` should only be updating any stale `TextLayoutInfo` components. ## Solution * Add a new method `update_text_layout_info` to `TextPipeline`. This method updates the given `TextLayoutInfo` without performing any shaping. * Call `update_text_layout_info` instead of `queue_text` from `text_system`. * Only query for `TextFont`, instead of the full `TextUiReader`. # The next step is to remove `TextPipeline::queue_text`. I didn't do that here as it's a fairly large refactor and I have a bunch of other open text PRs I'd like to get merged first. ## Testing #### yellow = this PR, red = main ``` cargo run --example many_glyphs --release --features trace_tracy,debug -- --no-text2d --recompute-text ``` Screenshot 2025-11-28 130411 ``` cargo run --example many_buttons --release --features trace_tracy,debug -- --text --respawn ``` Screenshot 2025-11-28 145240 --- crates/bevy_text/src/pipeline.rs | 190 +++++++++++++++++++++++++++++- crates/bevy_ui/src/widget/text.rs | 96 +++++++-------- 2 files changed, 233 insertions(+), 53 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 5d3fe166d1f82..08d98e8dbea14 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -4,8 +4,11 @@ use bevy_asset::{AssetId, Assets}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource, - system::ResMut, + component::Component, + entity::Entity, + reflect::ReflectComponent, + resource::Resource, + system::{Query, ResMut}, }; use bevy_image::prelude::*; use bevy_log::{once, warn}; @@ -498,6 +501,189 @@ impl TextPipeline { .cloned() .map(|(id, _)| id) } + + /// Update [`TextLayoutInfo`] with the new [`PositionedGlyph`] layout. + pub fn update_text_layout_info<'a>( + &mut self, + layout_info: &mut TextLayoutInfo, + text_font_query: Query<&'a TextFont>, + scale_factor: f64, + font_atlas_set: &mut FontAtlasSet, + texture_atlases: &mut Assets, + textures: &mut Assets, + computed: &mut ComputedTextBlock, + font_system: &mut CosmicFontSystem, + swash_cache: &mut SwashCache, + bounds: TextBounds, + ) -> Result<(), TextError> { + layout_info.glyphs.clear(); + layout_info.run_geometry.clear(); + layout_info.size = Default::default(); + + self.glyph_info.clear(); + + for text_font in text_font_query.iter_many(computed.entities.iter().map(|e| e.entity)) { + let mut section_info = ( + text_font.font.id(), + text_font.font_smoothing, + text_font.font_size, + 0.0, + 0.0, + 0.0, + ); + + if let Some((id, _)) = self.map_handle_to_font_id.get(§ion_info.0) { + let weight = font_system + .db() + .face(*id) + .map(|f| f.weight) + .unwrap_or(cosmic_text::Weight::NORMAL); + if let Some(font) = font_system.get_font(*id, weight) { + let swash = font.as_swash(); + let metrics = swash.metrics(&[]); + let upem = metrics.units_per_em as f32; + let scalar = section_info.2 * scale_factor as f32 / upem; + section_info.3 = (metrics.strikeout_offset * scalar).round(); + section_info.4 = (metrics.stroke_size * scalar).round().max(1.); + section_info.5 = (metrics.underline_offset * scalar).round(); + } + } + self.glyph_info.push(section_info); + } + + let buffer = &mut computed.buffer; + buffer.set_size(font_system, bounds.width, bounds.height); + let box_size = buffer_dimensions(buffer); + + let result = buffer.layout_runs().try_for_each(|run| { + let mut current_section: Option = None; + let mut start = 0.; + let mut end = 0.; + let result = run + .glyphs + .iter() + .map(move |layout_glyph| (layout_glyph, run.line_y, run.line_i)) + .try_for_each(|(layout_glyph, line_y, line_i)| { + match current_section { + Some(section) => { + if section != layout_glyph.metadata { + layout_info.run_geometry.push(RunGeometry { + span_index: section, + bounds: Rect::new( + start, + run.line_top, + end, + run.line_top + run.line_height, + ), + strikethrough_y: (run.line_y - self.glyph_info[section].3) + .round(), + strikethrough_thickness: self.glyph_info[section].4, + underline_y: (run.line_y - self.glyph_info[section].5).round(), + underline_thickness: self.glyph_info[section].4, + }); + start = end.max(layout_glyph.x); + current_section = Some(layout_glyph.metadata); + } + end = layout_glyph.x + layout_glyph.w; + } + None => { + current_section = Some(layout_glyph.metadata); + start = layout_glyph.x; + end = start + layout_glyph.w; + } + } + + let mut temp_glyph; + let span_index = layout_glyph.metadata; + let font_id = self.glyph_info[span_index].0; + let font_smoothing = self.glyph_info[span_index].1; + + let layout_glyph = if font_smoothing == FontSmoothing::None { + // If font smoothing is disabled, round the glyph positions and sizes, + // effectively discarding all subpixel layout. + temp_glyph = layout_glyph.clone(); + temp_glyph.x = temp_glyph.x.round(); + temp_glyph.y = temp_glyph.y.round(); + temp_glyph.w = temp_glyph.w.round(); + temp_glyph.x_offset = temp_glyph.x_offset.round(); + temp_glyph.y_offset = temp_glyph.y_offset.round(); + temp_glyph.line_height_opt = temp_glyph.line_height_opt.map(f32::round); + + &temp_glyph + } else { + layout_glyph + }; + + let physical_glyph = layout_glyph.physical((0., 0.), 1.); + + let font_atlases = font_atlas_set + .entry(FontAtlasKey( + font_id, + physical_glyph.cache_key.font_size_bits, + font_smoothing, + )) + .or_default(); + + let atlas_info = get_glyph_atlas_info(font_atlases, physical_glyph.cache_key) + .map(Ok) + .unwrap_or_else(|| { + add_glyph_to_atlas( + font_atlases, + texture_atlases, + textures, + &mut font_system.0, + &mut swash_cache.0, + layout_glyph, + font_smoothing, + ) + })?; + + let texture_atlas = texture_atlases.get(atlas_info.texture_atlas).unwrap(); + let location = atlas_info.location; + let glyph_rect = texture_atlas.textures[location.glyph_index]; + let left = location.offset.x as f32; + let top = location.offset.y as f32; + let glyph_size = UVec2::new(glyph_rect.width(), glyph_rect.height()); + + // offset by half the size because the origin is center + let x = glyph_size.x as f32 / 2.0 + left + physical_glyph.x as f32; + let y = + line_y.round() + physical_glyph.y as f32 - top + glyph_size.y as f32 / 2.0; + + let position = Vec2::new(x, y); + + let pos_glyph = PositionedGlyph { + position, + size: glyph_size.as_vec2(), + atlas_info, + span_index, + byte_index: layout_glyph.start, + byte_length: layout_glyph.end - layout_glyph.start, + line_index: line_i, + }; + layout_info.glyphs.push(pos_glyph); + Ok(()) + }); + if let Some(section) = current_section { + layout_info.run_geometry.push(RunGeometry { + span_index: section, + bounds: Rect::new(start, run.line_top, end, run.line_top + run.line_height), + strikethrough_y: (run.line_y - self.glyph_info[section].3).round(), + strikethrough_thickness: self.glyph_info[section].4, + underline_y: (run.line_y - self.glyph_info[section].5).round(), + underline_thickness: self.glyph_info[section].4, + }); + } + + result + }); + + // Check result. + result?; + + layout_info.size = box_size; + Ok(()) + } } /// Render information for a corresponding text block. diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c84b463ddf3cb..c132bee60165d 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -319,72 +319,66 @@ pub fn measure_text_system( /// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`]. pub fn text_system( mut textures: ResMut>, - fonts: Res>, mut texture_atlases: ResMut>, mut font_atlas_set: ResMut, mut text_pipeline: ResMut, mut text_query: Query<( - Entity, Ref, &TextLayout, &mut TextLayoutInfo, &mut TextNodeFlags, &mut ComputedTextBlock, )>, - mut text_reader: TextUiReader, + text_font_query: Query<&TextFont>, mut font_system: ResMut, mut swash_cache: ResMut, ) { - for (entity, node, block, text_layout_info, mut text_flags, mut computed) in &mut text_query { - // Skip the text node if it is waiting for a new measure func - if text_flags.needs_measure_fn { - continue; - } - - if !(node.is_changed() || text_flags.needs_recompute) { - continue; - } + for (node, block, mut text_layout_info, mut text_flags, mut computed) in &mut text_query { + if node.is_changed() || text_flags.needs_recompute { + // Skip the text node if it is waiting for a new measure func + if text_flags.needs_measure_fn { + continue; + } - let physical_node_size = if block.linebreak == LineBreak::NoWrap { - // With `NoWrap` set, no constraints are placed on the width of the text. - TextBounds::UNBOUNDED - } else { - // `scale_factor` is already multiplied by `UiScale` - TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) - }; + let scale_factor = node.inverse_scale_factor().recip().into(); + let physical_node_size = if block.linebreak == LineBreak::NoWrap { + // With `NoWrap` set, no constraints are placed on the width of the text. + TextBounds::UNBOUNDED + } else { + // `scale_factor` is already multiplied by `UiScale` + TextBounds::new(node.unrounded_size.x, node.unrounded_size.y) + }; - let text_layout_info = text_layout_info.into_inner(); - match text_pipeline.queue_text( - text_layout_info, - &fonts, - text_reader.iter(entity), - node.inverse_scale_factor.recip() as f64, - block, - physical_node_size, - &mut font_atlas_set, - &mut texture_atlases, - &mut textures, - &mut computed, - &mut font_system, - &mut swash_cache, - ) { - Err(TextError::NoSuchFont) => { - // There was an error processing the text layout, try again next frame - text_flags.needs_recompute = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - Ok(()) => { - text_layout_info.scale_factor = node.inverse_scale_factor.recip(); - text_layout_info.size *= node.inverse_scale_factor; - text_flags.needs_recompute = false; + match text_pipeline.update_text_layout_info( + &mut text_layout_info, + text_font_query, + scale_factor, + &mut font_atlas_set, + &mut texture_atlases, + &mut textures, + &mut computed, + &mut font_system, + &mut swash_cache, + physical_node_size, + ) { + Err(TextError::NoSuchFont) => { + // There was an error processing the text layout, try again next frame + text_flags.needs_recompute = true; + } + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToGetGlyphImage(_) + | TextError::MissingAtlasLayout + | TextError::MissingAtlasTexture + | TextError::InconsistentAtlasState), + ) => { + panic!("Fatal error when processing text: {e}."); + } + Ok(()) => { + text_layout_info.scale_factor = scale_factor as f32; + text_layout_info.size *= node.inverse_scale_factor(); + text_flags.needs_recompute = false; + } } } } From 008ca5d1b3fb99ae866d9dfcf02e399f7e17d32e Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sat, 6 Dec 2025 18:39:24 -0800 Subject: [PATCH 022/133] Remove `getrandom` dependency from `bevy_platform`. (#21971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #21336. `bevy_platform` and dependents such as `bevy_ecs` may be used on web without incurring the `RUSTFLAGS` requirement of `getrandom`. ## Solution Remove dependency `bevy_platform` → `getrandom`. Note: There was a suggestion of adding a dependency `bevy_math` → `getrandom` to replace it, but `bevy_math` does not actually depend transitively on `getrandom` at all (except in its examples), nor does any other Bevy library. If making `getrandom` work is desired for convenience of Bevy users, we could add `bevy_internal/web` → `getrandom/wasm_js`, but that would not be for the use of Bevy itself at all, and it would mean that Bevy users would hit the `RUSTFLAGS` requirement even if they don't truly need it. ## Testing - Ran `cargo run -p ci -- compile` and `cargo run -p ci -- test`. - Tested depending on this version of `bevy_ecs` in my own wasm32 project. - Have not tested any further feature combinations. --- crates/bevy_platform/Cargo.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index c428a6bf7b79d..e17d5a4bc0c72 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -50,7 +50,6 @@ critical-section = ["dep:critical-section", "portable-atomic/critical-section"] web = [ "std", "dep:web-time", - "dep:getrandom", "dep:wasm-bindgen-futures", "dep:wasm-bindgen", "dep:js-sys", @@ -76,9 +75,6 @@ rayon = { version = "1", default-features = false, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] web-time = { version = "1.1", default-features = false, optional = true } -getrandom = { version = "0.3.0", default-features = false, optional = true, features = [ - "wasm_js", -] } wasm-bindgen-futures = { version = "0.4", default-features = false, optional = true } futures-channel = { version = "0.3", default-features = false } js-sys = { version = "0.3", default-features = false, optional = true } From ad1c38a3f9624b7883075e140eebe4977ed7fa53 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 7 Dec 2025 10:00:57 -0800 Subject: [PATCH 023/133] Fix the doc test for WebAssetPlugin (#21968) # Objective - Fix `cargo t -p bevy_asset --all-features`. - Note: `all-features` do not run on CI! - The doc tests for the `WebAssetPlugin` don't compile at all. There are bad imports and we don't even add the `AssetPlugin`. ## Solution - Add missing imports, and add missing plugins to the `App`. ## Testing - Ran `cargo t -p bevy_asset --all-features` and it passes now! --- crates/bevy_asset/src/io/web.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/bevy_asset/src/io/web.rs b/crates/bevy_asset/src/io/web.rs index 196e36325c1a2..e6ec6d615bf3a 100644 --- a/crates/bevy_asset/src/io/web.rs +++ b/crates/bevy_asset/src/io/web.rs @@ -23,12 +23,12 @@ use tracing::warn; /// Example usage: /// /// ```rust -/// # use bevy_app::{App, Startup}; -/// # use bevy_ecs::prelude::{Commands, Res}; -/// # use bevy_asset::web::{WebAssetPlugin, AssetServer}; +/// # use bevy_app::{App, Startup, TaskPoolPlugin}; +/// # use bevy_ecs::prelude::{Commands, Component, Res}; +/// # use bevy_asset::{Asset, AssetApp, AssetPlugin, AssetServer, Handle, io::web::WebAssetPlugin}; +/// # use bevy_reflect::TypePath; /// # struct DefaultPlugins; -/// # impl DefaultPlugins { fn set(plugin: WebAssetPlugin) -> WebAssetPlugin { plugin } } -/// # use bevy_asset::web::AssetServer; +/// # impl DefaultPlugins { fn set(&self, plugin: WebAssetPlugin) -> WebAssetPlugin { plugin } } /// # #[derive(Asset, TypePath, Default)] /// # struct Image; /// # #[derive(Component)] @@ -39,6 +39,8 @@ use tracing::warn; /// .add_plugins(DefaultPlugins.set(WebAssetPlugin { /// silence_startup_warning: true, /// })) +/// # .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) +/// # .init_asset::() /// # .add_systems(Startup, setup).run(); /// # } /// // ... From 25d4cedfc686bead62a7cbeb85a8312e7d8e1a5c Mon Sep 17 00:00:00 2001 From: Chris Biscardi Date: Sun, 7 Dec 2025 10:18:36 -0800 Subject: [PATCH 024/133] enable easier modification of image samplers in image loaders (#22042) # Objective Previously, to modify a sampler when loading an image with settings it was required to set all fields individually. Much of the time, these settings are all the same so to enable repeating textures you have to set the address_mode 3 times. To set filters you have to set them another 3 times. ```rust MeshMaterial3d(materials.add( StandardMaterial { base_color_texture: Some( asset_server.load_with_settings( "floor_graph_base_color.png", |settings: &mut ImageLoaderSettings| { let descriptor = settings.sampler.get_or_init_descriptor(); descriptor.address_mode_u = bevy::image::ImageAddressMode::Repeat; descriptor.address_mode_v = bevy::image::ImageAddressMode::Repeat; descriptor.address_mode_w = bevy::image::ImageAddressMode::Repeat; descriptor.mag_filter = ImageFilterMode::Linear; descriptor.min_filter = ImageFilterMode::Linear; descriptor.mipmap_filter = ImageFilterMode::Linear; } ), ), unlit: true, cull_mode: None, uv_transform: Affine2::from_scale(Vec2::new(30., 90.)), ..default() }, )), ``` ## Solution Add two new helpers, loosely modeled after the `Transform::with_` functions. Further modifications can still be made to the descriptor for use cases which want to set one of the three fields differently, or additionally set anisotropy_clamp, etc. ```rust let descriptor = settings .sampler .get_or_init_descriptor() .set_address_mode(ImageAddressMode::Repeat) .set_filter(ImageFilterMode::Linear); ``` ## Testing New test in bevy_image ## Showcase ```rust MeshMaterial3d(materials.add( StandardMaterial { base_color_texture: Some( asset_server.load_with_settings( "floor_graph_base_color.png", |settings: &mut ImageLoaderSettings| { let descriptor = settings .sampler .get_or_init_descriptor() .set_address_mode(ImageAddressMode::Repeat) .set_filter(ImageFilterMode::Linear); } ), ), unlit: true, cull_mode: None, uv_transform: Affine2::from_scale(Vec2::new(30., 90.)), ..default() }, )), ``` This PR now includes: - `set_filter` - `set_address_mode` - `set_anisotropic_filter` --------- Co-authored-by: Greeble <166992735+greeble-dev@users.noreply.github.com> --- crates/bevy_image/src/image.rs | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index b296ecdf9388c..aeeb58b1a7d98 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -699,6 +699,36 @@ impl ImageSamplerDescriptor { } } + /// Returns this sampler descriptor with a new `ImageFilterMode` min, mag, and mipmap filters + #[inline] + pub fn set_filter(&mut self, filter: ImageFilterMode) -> &mut Self { + self.mag_filter = filter; + self.min_filter = filter; + self.mipmap_filter = filter; + self + } + + /// Returns this sampler descriptor with a new `ImageAddressMode` for u, v, and w + #[inline] + pub fn set_address_mode(&mut self, address_mode: ImageAddressMode) -> &mut Self { + self.address_mode_u = address_mode; + self.address_mode_v = address_mode; + self.address_mode_w = address_mode; + self + } + + /// Returns this sampler descriptor with an `anisotropy_clamp` value and also + /// set filters to `ImageFilterMode::Linear`, which is required to + /// use anisotropy. + #[inline] + pub fn set_anisotropic_filter(&mut self, anisotropy_clamp: u16) -> &mut Self { + self.mag_filter = ImageFilterMode::Linear; + self.min_filter = ImageFilterMode::Linear; + self.mipmap_filter = ImageFilterMode::Linear; + self.anisotropy_clamp = anisotropy_clamp; + self + } + pub fn as_wgpu(&self) -> SamplerDescriptor> { SamplerDescriptor { label: self.label.as_deref(), @@ -2212,4 +2242,34 @@ mod test { assert!(image.data.as_ref().unwrap().iter().all(|&p| p == 255)); } + + #[test] + fn get_or_init_sampler_modifications() { + // given some sampler + let mut default_sampler = ImageSampler::Default; + // a load_with_settings call wants to customize the descriptor + let my_sampler_in_a_loader = default_sampler + .get_or_init_descriptor() + .set_filter(ImageFilterMode::Linear) + .set_address_mode(ImageAddressMode::Repeat); + + assert_eq!( + my_sampler_in_a_loader.address_mode_u, + ImageAddressMode::Repeat + ); + assert_eq!(my_sampler_in_a_loader.min_filter, ImageFilterMode::Linear); + } + + #[test] + fn get_or_init_sampler_anisotropy() { + // given some sampler + let mut default_sampler = ImageSampler::Default; + // a load_with_settings call wants to customize the descriptor + let my_sampler_in_a_loader = default_sampler + .get_or_init_descriptor() + .set_anisotropic_filter(8); + + assert_eq!(my_sampler_in_a_loader.min_filter, ImageFilterMode::Linear); + assert_eq!(my_sampler_in_a_loader.anisotropy_clamp, 8); + } } From 89e41937dd57b824ab8397e8c282b5eab0a4e68a Mon Sep 17 00:00:00 2001 From: JonasJebing <34072795+JonasJebing@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:20:44 +0100 Subject: [PATCH 025/133] add missing `Iterator::size_hint` overrides, most of which cause `ExactSizeIterator::len` to panic (#22035) # Objective - Fixes `ExactSizeIterator::len` panic in a few of these iterators. Note: the std default impl of `ExactSizeIterator::len` has `assert_eq!(Some(lower), upper);`. I am facing this issue myself with `::len`. - Should slightly improve performance in some cases, where the more precise `size_hint` causes less allocations (for example when collecting into a `Vec`) ## Solution - Add `size_hint` overrides that forward to inner iterators or an `ExactSizeIterator::len` override in one specific case. ## Testing - Since the overrides are pretty simple, I mostly didn't add any tests. - I didn't feel 100% confident in the `WriteBatchIds::size_hint` implementation, so I added a test there. --- crates/bevy_ecs/src/entity/hash_map.rs | 8 ++++++++ crates/bevy_ecs/src/entity/hash_set.rs | 16 ++++++++++++++++ crates/bevy_ecs/src/entity/index_map.rs | 24 ++++++++++++++++++++++++ crates/bevy_ecs/src/entity/index_set.rs | 12 ++++++++++++ crates/bevy_ecs/src/message/messages.rs | 20 ++++++++++++++++++++ crates/bevy_mesh/src/index.rs | 9 +++++++++ 6 files changed, 89 insertions(+) diff --git a/crates/bevy_ecs/src/entity/hash_map.rs b/crates/bevy_ecs/src/entity/hash_map.rs index d83ea7bae1dcc..505cc440f117f 100644 --- a/crates/bevy_ecs/src/entity/hash_map.rs +++ b/crates/bevy_ecs/src/entity/hash_map.rs @@ -176,6 +176,10 @@ impl<'a, V> Iterator for Keys<'a, V> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl ExactSizeIterator for Keys<'_, V> {} @@ -234,6 +238,10 @@ impl Iterator for IntoKeys { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl ExactSizeIterator for IntoKeys {} diff --git a/crates/bevy_ecs/src/entity/hash_set.rs b/crates/bevy_ecs/src/entity/hash_set.rs index 7fd1ae9011273..8b2e5927e813e 100644 --- a/crates/bevy_ecs/src/entity/hash_set.rs +++ b/crates/bevy_ecs/src/entity/hash_set.rs @@ -238,6 +238,10 @@ impl<'a> Iterator for Iter<'a> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl ExactSizeIterator for Iter<'_> {} @@ -293,6 +297,10 @@ impl Iterator for IntoIter { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl ExactSizeIterator for IntoIter {} @@ -345,6 +353,10 @@ impl<'a> Iterator for Drain<'a> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl ExactSizeIterator for Drain<'_> {} @@ -394,6 +406,10 @@ impl<'a, F: FnMut(&Entity) -> bool> Iterator for ExtractIf<'a, F> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl bool> FusedIterator for ExtractIf<'_, F> {} diff --git a/crates/bevy_ecs/src/entity/index_map.rs b/crates/bevy_ecs/src/entity/index_map.rs index 3e94f51362c29..1afd28c12b2a8 100644 --- a/crates/bevy_ecs/src/entity/index_map.rs +++ b/crates/bevy_ecs/src/entity/index_map.rs @@ -861,6 +861,10 @@ impl<'a, V> Iterator for Iter<'a, V> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for Iter<'_, V> { @@ -934,6 +938,10 @@ impl<'a, V> Iterator for IterMut<'a, V> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for IterMut<'_, V> { @@ -1004,6 +1012,10 @@ impl Iterator for IntoIter { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for IntoIter { @@ -1072,6 +1084,10 @@ impl Iterator for Drain<'_, V> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for Drain<'_, V> { @@ -1120,6 +1136,10 @@ impl<'a, V> Iterator for Keys<'a, V> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for Keys<'_, V> { @@ -1188,6 +1208,10 @@ impl Iterator for IntoKeys { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for IntoKeys { diff --git a/crates/bevy_ecs/src/entity/index_set.rs b/crates/bevy_ecs/src/entity/index_set.rs index 42f420a211a23..73bbecde6688b 100644 --- a/crates/bevy_ecs/src/entity/index_set.rs +++ b/crates/bevy_ecs/src/entity/index_set.rs @@ -582,6 +582,10 @@ impl<'a> Iterator for Iter<'a> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for Iter<'_> { @@ -651,6 +655,10 @@ impl Iterator for IntoIter { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for IntoIter { @@ -723,6 +731,10 @@ impl<'a> Iterator for Drain<'a> { fn next(&mut self) -> Option { self.0.next() } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } } impl DoubleEndedIterator for Drain<'_> { diff --git a/crates/bevy_ecs/src/message/messages.rs b/crates/bevy_ecs/src/message/messages.rs index 39a3992298023..bc708c734daeb 100644 --- a/crates/bevy_ecs/src/message/messages.rs +++ b/crates/bevy_ecs/src/message/messages.rs @@ -382,6 +382,11 @@ impl Iterator for WriteBatchIds { result } + + fn size_hint(&self) -> (usize, Option) { + let len = ::len(self); + (len, Some(len)) + } } impl ExactSizeIterator for WriteBatchIds { @@ -425,4 +430,19 @@ mod tests { assert_eq!(test_messages.len(), 2); // Messages are double-buffered, so we see 2 + 0 = 2 assert_eq!(test_messages.iter_current_update_messages().count(), 0); } + + #[test] + fn write_batch_iter_size_hint() { + #[derive(Message, Clone, Copy)] + struct TestMessage; + + let mut test_messages = Messages::::default(); + let write_batch_ids = test_messages.write_batch([TestMessage; 4]); + let expected_len = 4; + assert_eq!(write_batch_ids.len(), expected_len); + assert_eq!( + write_batch_ids.size_hint(), + (expected_len, Some(expected_len)) + ); + } } diff --git a/crates/bevy_mesh/src/index.rs b/crates/bevy_mesh/src/index.rs index 87d81cc3f2fa2..6f89f2dfd59a1 100644 --- a/crates/bevy_mesh/src/index.rs +++ b/crates/bevy_mesh/src/index.rs @@ -33,6 +33,15 @@ where FourIterators::Fourth(iter) => iter.next(), } } + + fn size_hint(&self) -> (usize, Option) { + match self { + FourIterators::First(iter) => iter.size_hint(), + FourIterators::Second(iter) => iter.size_hint(), + FourIterators::Third(iter) => iter.size_hint(), + FourIterators::Fourth(iter) => iter.size_hint(), + } + } } /// An error that occurred while trying to invert the winding of a [`Mesh`](super::Mesh). From 84e12788bd099623f02a8d97189e5a73b2537e2d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 7 Dec 2025 18:21:57 +0000 Subject: [PATCH 026/133] Update size and metrics after `set_rich_text` in `TextPipeline::update_buffer` (#22021) # Objective Rearrange the order that cosmic text's properties are set for improved performance. ## Solution Update size and metrics after `set_rich_text` in `TextPipeline::update_buffer` ## Testing yellow = this PR, red = main: ``` cargo run --example many_glyphs --features=trace_tracy,debug --release -- --recompute-text ``` reorder reorder-measure-text --- crates/bevy_text/src/pipeline.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 08d98e8dbea14..e175c93f13791 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -191,7 +191,6 @@ impl TextPipeline { // Update the buffer. let buffer = &mut computed.buffer; - buffer.set_size(font_system, bounds.width, bounds.height); buffer.set_wrap( font_system, @@ -213,11 +212,10 @@ impl TextPipeline { // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != Justify::Left { - let dimensions = buffer_dimensions(buffer); - // `set_size` causes a re-layout to occur. - buffer.set_size(font_system, Some(dimensions.x), bounds.height); - } + let width = (bounds.width.is_none() && justify != Justify::Left) + .then(|| buffer_dimensions(buffer).x) + .or(bounds.width); + buffer.set_size(font_system, width, bounds.height); // Recover the spans buffer. spans.clear(); From 9f4b18d00eb7c6f90a37a4d822e89226846d05d8 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 7 Dec 2025 10:25:26 -0800 Subject: [PATCH 027/133] Release the asset infos lock before acquiring the file transaction lock. (#22017) # Objective - Fix the first flake related to #22001. - We had a double lock problem. We would first lock `asset_infos`, then lock one of the assets within `asset_infos` **and then dropped the `asset_infos` lock**. This means however that if a process needs to access `asset_infos` (which is necessary for loading nested assets during processing), we'll have one thread trying to lock 1) asset_infos, 2) per-asset lock, and we'll have another thread trying to lock 1) per-asset lock (we've already dropped the asset_infos lock), 2) asset_infos. A classic deadlock! ## Solution - Before locking the per-asset lock, we clone the `Arc` out of the `asset_infos`, drop the `asset_infos`, and only then lock the per-asset lock. This ensures that we never hang on the `asset_infos` lock when just trying to fetch a single asset. - Make all the access to `asset_infos` "short" - they get what they need out of `asset_infos` and then drop the lock as soon as possible. - I also used `?` to cleanup some methods in `ProcessorAssetInfos`, where previously it was "remove the asset info, then have one big `if let` after it". Now we just return early if the value is none. - I also happened to fix a weird case where the new path in a rename wasn't guarded by a transaction lock. Now it is! ## Testing - Running without this fix I get "Ran out of loops" from the `only_reprocesses_wrong_hash_on_startup` test quite quickly (after a minute or so). With this fix, I now only get the assertion failure problem. If I also skip that assertion, the test hasn't flaked for a while! Yay, no more deadlock! --- crates/bevy_asset/src/processor/mod.rs | 233 ++++++++++++++----------- 1 file changed, 131 insertions(+), 102 deletions(-) diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 516f83448be1f..8e58b120fb934 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -642,15 +642,20 @@ impl AssetProcessor { async fn handle_removed_asset(&self, source: &AssetSource, path: PathBuf) { let asset_path = AssetPath::from(path).with_source(source.id()); debug!("Removing processed {asset_path} because source was removed"); - let mut infos = self.data.processing_state.asset_infos.write().await; - if let Some(info) = infos.get(&asset_path) { - // we must wait for uncontested write access to the asset source to ensure existing readers / writers - // can finish their operations - let _write_lock = info.file_transaction_lock.write(); - self.remove_processed_asset_and_meta(source, asset_path.path()) - .await; - } - infos.remove(&asset_path).await; + let lock = { + // Scope the infos lock so we don't hold up other processing for too long. + let mut infos = self.data.processing_state.asset_infos.write().await; + infos.remove(&asset_path).await + }; + let Some(lock) = lock else { + return; + }; + + // we must wait for uncontested write access to the asset source to ensure existing + // readers/writers can finish their operations + let _write_lock = lock.write(); + self.remove_processed_asset_and_meta(source, asset_path.path()) + .await; } /// Handles a renamed source asset by moving its processed results to the new location and updating in-memory paths + metadata. @@ -662,24 +667,29 @@ impl AssetProcessor { new: PathBuf, new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>, ) { - let mut infos = self.data.processing_state.asset_infos.write().await; let old = AssetPath::from(old).with_source(source.id()); let new = AssetPath::from(new).with_source(source.id()); let processed_writer = source.processed_writer().unwrap(); - if let Some(info) = infos.get(&old) { - // we must wait for uncontested write access to the asset source to ensure existing readers / writers - // can finish their operations - let _write_lock = info.file_transaction_lock.write(); - processed_writer - .rename(old.path(), new.path()) - .await - .unwrap(); - processed_writer - .rename_meta(old.path(), new.path()) - .await - .unwrap(); - } - infos.rename(&old, &new, new_task_sender).await; + let result = { + // Scope the infos lock so we don't hold up other processing for too long. + let mut infos = self.data.processing_state.asset_infos.write().await; + infos.rename(&old, &new, new_task_sender).await + }; + let Some((old_lock, new_lock)) = result else { + return; + }; + // we must wait for uncontested write access to both assets to ensure existing + // readers/writers can finish their operations + let _old_write_lock = old_lock.write(); + let _new_write_lock = new_lock.write(); + processed_writer + .rename(old.path(), new.path()) + .await + .unwrap(); + processed_writer + .rename_meta(old.path(), new.path()) + .await + .unwrap(); } async fn queue_processing_tasks_for_folder( @@ -1069,9 +1079,15 @@ impl AssetProcessor { // Note: this lock must remain alive until all processed asset and meta writes have finished (or failed) // See ProcessedAssetInfo::file_transaction_lock docs for more info let _transaction_lock = { - let mut infos = self.data.processing_state.asset_infos.write().await; - let info = infos.get_or_insert(asset_path.clone()); - info.file_transaction_lock.write_arc().await + let lock = { + let mut infos = self.data.processing_state.asset_infos.write().await; + let info = infos.get_or_insert(asset_path.clone()); + // Clone out the transaction lock first and then lock after we've dropped the + // asset_infos. Otherwise, trying to lock a single path can block all other paths to + // (leading to deadlocks). + info.file_transaction_lock.clone() + }; + lock.write_arc().await }; // NOTE: if processing the asset fails this will produce an "unfinished" log entry, forcing a rebuild on next run. @@ -1317,11 +1333,17 @@ impl ProcessingState { &self, path: &AssetPath<'static>, ) -> Result, AssetReaderError> { - let infos = self.asset_infos.read().await; - let info = infos - .get(path) - .ok_or_else(|| AssetReaderError::NotFound(path.path().to_owned()))?; - Ok(info.file_transaction_lock.read_arc().await) + let lock = { + let infos = self.asset_infos.read().await; + let info = infos + .get(path) + .ok_or_else(|| AssetReaderError::NotFound(path.path().to_owned()))?; + // Clone out the transaction lock first and then lock after we've dropped the + // asset_infos. Otherwise, trying to lock a single path can block all other paths to + // (leading to deadlocks). + info.file_transaction_lock.clone() + }; + Ok(lock.read_arc().await) } /// Returns a future that will not finish until the path has been processed. @@ -1603,95 +1625,102 @@ impl ProcessorAssetInfos { } } - /// Remove the info for the given path. This should only happen if an asset's source is removed / non-existent - async fn remove(&mut self, asset_path: &AssetPath<'static>) { - let info = self.infos.remove(asset_path); - if let Some(info) = info { - if let Some(processed_info) = info.processed_info { - self.clear_dependencies(asset_path, processed_info); - } - // Tell all listeners this asset does not exist - info.status_sender - .broadcast(ProcessStatus::NonExistent) - .await - .unwrap(); - if !info.dependents.is_empty() { - error!( + /// Remove the info for the given path. This should only happen if an asset's source is + /// removed/non-existent. Returns the transaction lock for the asset, or [`None`] if the asset + /// path does not exist. + async fn remove( + &mut self, + asset_path: &AssetPath<'static>, + ) -> Option>> { + let info = self.infos.remove(asset_path)?; + if let Some(processed_info) = info.processed_info { + self.clear_dependencies(asset_path, processed_info); + } + // Tell all listeners this asset does not exist + info.status_sender + .broadcast(ProcessStatus::NonExistent) + .await + .unwrap(); + if !info.dependents.is_empty() { + error!( "The asset at {asset_path} was removed, but it had assets that depend on it to be processed. Consider updating the path in the following assets: {:?}", info.dependents ); - self.non_existent_dependents - .insert(asset_path.clone(), info.dependents); - } + self.non_existent_dependents + .insert(asset_path.clone(), info.dependents); } + + Some(info.file_transaction_lock) } - /// Remove the info for the given path. This should only happen if an asset's source is removed / non-existent + /// Remove the info for the old path, and move over its info to the new path. This should only + /// happen if an asset's source is removed/non-existent. Returns the transaction locks for the + /// old and new assets respectively, or [`None`] if the old path does not exist. async fn rename( &mut self, old: &AssetPath<'static>, new: &AssetPath<'static>, new_task_sender: &async_channel::Sender<(AssetSourceId<'static>, PathBuf)>, - ) { - let info = self.infos.remove(old); - if let Some(mut info) = info { - if !info.dependents.is_empty() { - // TODO: We can't currently ensure "moved" folders with relative paths aren't broken because AssetPath - // doesn't distinguish between absolute and relative paths. We have "erased" relativeness. In the short term, - // we could do "remove everything in a folder and re-add", but that requires full rebuilds / destroying the cache. - // If processors / loaders could enumerate dependencies, we could check if the new deps line up with a rename. - // If deps encoded "relativeness" as part of loading, that would also work (this seems like the right call). - // TODO: it would be nice to log an error here for dependents that aren't also being moved + fixed. - // (see the remove impl). - error!( + ) -> Option<(Arc>, Arc>)> { + let mut info = self.infos.remove(old)?; + if !info.dependents.is_empty() { + // TODO: We can't currently ensure "moved" folders with relative paths aren't broken because AssetPath + // doesn't distinguish between absolute and relative paths. We have "erased" relativeness. In the short term, + // we could do "remove everything in a folder and re-add", but that requires full rebuilds / destroying the cache. + // If processors / loaders could enumerate dependencies, we could check if the new deps line up with a rename. + // If deps encoded "relativeness" as part of loading, that would also work (this seems like the right call). + // TODO: it would be nice to log an error here for dependents that aren't also being moved + fixed. + // (see the remove impl). + error!( "The asset at {old} was removed, but it had assets that depend on it to be processed. Consider updating the path in the following assets: {:?}", info.dependents ); - self.non_existent_dependents - .insert(old.clone(), core::mem::take(&mut info.dependents)); - } - if let Some(processed_info) = &info.processed_info { - // Update "dependent" lists for this asset's "process dependencies" to use new path. - for dep in &processed_info.process_dependencies { - if let Some(info) = self.infos.get_mut(&dep.path) { - info.dependents.remove(old); - info.dependents.insert(new.clone()); - } else if let Some(dependents) = self.non_existent_dependents.get_mut(&dep.path) - { - dependents.remove(old); - dependents.insert(new.clone()); - } + self.non_existent_dependents + .insert(old.clone(), core::mem::take(&mut info.dependents)); + } + if let Some(processed_info) = &info.processed_info { + // Update "dependent" lists for this asset's "process dependencies" to use new path. + for dep in &processed_info.process_dependencies { + if let Some(info) = self.infos.get_mut(&dep.path) { + info.dependents.remove(old); + info.dependents.insert(new.clone()); + } else if let Some(dependents) = self.non_existent_dependents.get_mut(&dep.path) { + dependents.remove(old); + dependents.insert(new.clone()); } } - // Tell all listeners this asset no longer exists - info.status_sender - .broadcast(ProcessStatus::NonExistent) - .await - .unwrap(); - let dependents: Vec> = { - let new_info = self.get_or_insert(new.clone()); - new_info.processed_info = info.processed_info; - new_info.status = info.status; - // Ensure things waiting on the new path are informed of the status of this asset - if let Some(status) = new_info.status { - new_info.status_sender.broadcast(status).await.unwrap(); - } - new_info.dependents.iter().cloned().collect() - }; - // Queue the asset for a reprocess check, in case it needs new meta. + } + // Tell all listeners this asset no longer exists + info.status_sender + .broadcast(ProcessStatus::NonExistent) + .await + .unwrap(); + let new_info = self.get_or_insert(new.clone()); + new_info.processed_info = info.processed_info; + new_info.status = info.status; + // Ensure things waiting on the new path are informed of the status of this asset + if let Some(status) = new_info.status { + new_info.status_sender.broadcast(status).await.unwrap(); + } + let dependents = new_info.dependents.iter().cloned().collect::>(); + // Queue the asset for a reprocess check, in case it needs new meta. + let _ = new_task_sender + .send((new.source().clone_owned(), new.path().to_owned())) + .await; + for dependent in dependents { + // Queue dependents for reprocessing because they might have been waiting for this asset. let _ = new_task_sender - .send((new.source().clone_owned(), new.path().to_owned())) + .send(( + dependent.source().clone_owned(), + dependent.path().to_owned(), + )) .await; - for dependent in dependents { - // Queue dependents for reprocessing because they might have been waiting for this asset. - let _ = new_task_sender - .send(( - dependent.source().clone_owned(), - dependent.path().to_owned(), - )) - .await; - } } + + Some(( + info.file_transaction_lock, + new_info.file_transaction_lock.clone(), + )) } fn clear_dependencies(&mut self, asset_path: &AssetPath<'static>, removed_info: ProcessedInfo) { From 149cbabda7d99e54200a69aaba42a976a9fdf845 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 7 Dec 2025 18:27:00 +0000 Subject: [PATCH 028/133] compute the text buffer size during layout updates (#22010) # Objective `queue_text` does a second pass to compute the size of the text buffer, this could be moved inside the layout update loop instead. ## Solution Do the buffer size computation inside the update loop. --- crates/bevy_text/src/pipeline.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index e175c93f13791..f3ad73b1c9d99 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -313,9 +313,11 @@ impl TextPipeline { } let buffer = &mut computed.buffer; - let box_size = buffer_dimensions(buffer); + let mut box_size = Vec2::ZERO; let result = buffer.layout_runs().try_for_each(|run| { + box_size.x = box_size.x.max(run.line_w); + box_size.y += run.line_height; let mut current_section: Option = None; let mut start = 0.; let mut end = 0.; @@ -441,7 +443,7 @@ impl TextPipeline { // Check result. result?; - layout_info.size = box_size; + layout_info.size = box_size.ceil(); Ok(()) } @@ -551,9 +553,11 @@ impl TextPipeline { let buffer = &mut computed.buffer; buffer.set_size(font_system, bounds.width, bounds.height); - let box_size = buffer_dimensions(buffer); + let mut box_size = Vec2::ZERO; let result = buffer.layout_runs().try_for_each(|run| { + box_size.x = box_size.x.max(run.line_w); + box_size.y += run.line_height; let mut current_section: Option = None; let mut start = 0.; let mut end = 0.; From 49d44332a53679f69eda380e9577222d55a65175 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 7 Dec 2025 18:28:35 +0000 Subject: [PATCH 029/133] `TextLayoutInfo::clear` (#22006) # Objective Bevy text cosmic text cleanup, add a `clear` method to `TextLayoutInfo`. ## Solution Add a `clear` method to `TextLayoutInfo` instead of manual resetting each field --- crates/bevy_text/src/pipeline.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f3ad73b1c9d99..40a428510dfa8 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -254,9 +254,7 @@ impl TextPipeline { font_system: &mut CosmicFontSystem, swash_cache: &mut SwashCache, ) -> Result<(), TextError> { - layout_info.glyphs.clear(); - layout_info.run_geometry.clear(); - layout_info.size = Default::default(); + layout_info.clear(); // Clear this here at the focal point of text rendering to ensure the field's lifecycle has strong boundaries. computed.needs_rerender = false; @@ -709,6 +707,16 @@ pub struct TextLayoutInfo { pub size: Vec2, } +impl TextLayoutInfo { + /// Clear the layout, retaining capacity + pub fn clear(&mut self) { + self.scale_factor = 1.; + self.glyphs.clear(); + self.run_geometry.clear(); + self.size = Vec2::ZERO; + } +} + /// Geometry of a text run used to render text decorations like background colors, strikethrough, and underline. /// A run in `bevy_text` is a contiguous sequence of glyphs on a line that share the same text attributes like font, /// font size, and line height. From ad15fdaa5f09f9683792b10e7931e97605787738 Mon Sep 17 00:00:00 2001 From: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:30:43 -0600 Subject: [PATCH 030/133] Fix unsound `&EntityMutExcept` to `FilteredEntityMut` conversion (#21982) # Objective Fix an effectively `& &mut` -> `&mut` conversion. ## Solution Switch it to `&mut &mut` -> `&mut`. --- crates/bevy_ecs/src/world/entity_access/except.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/world/entity_access/except.rs b/crates/bevy_ecs/src/world/entity_access/except.rs index 92d372e26bd81..6a2261dbf2186 100644 --- a/crates/bevy_ecs/src/world/entity_access/except.rs +++ b/crates/bevy_ecs/src/world/entity_access/except.rs @@ -429,8 +429,8 @@ where } } -impl<'w, 's, B: Bundle> From<&'w EntityMutExcept<'_, 's, B>> for FilteredEntityMut<'w, 's> { - fn from(value: &'w EntityMutExcept<'_, 's, B>) -> Self { +impl<'w, 's, B: Bundle> From<&'w mut EntityMutExcept<'_, 's, B>> for FilteredEntityMut<'w, 's> { + fn from(value: &'w mut EntityMutExcept<'_, 's, B>) -> Self { // SAFETY: // - The FilteredEntityMut has the same component access as the given EntityMutExcept. unsafe { FilteredEntityMut::new(value.entity, value.access) } From 7a7fe9f301634e2fcccb73e160286a70d4e275db Mon Sep 17 00:00:00 2001 From: databasedav <31483365+databasedav@users.noreply.github.com> Date: Sun, 7 Dec 2025 10:52:57 -0800 Subject: [PATCH 031/133] remove newline between `CursorIcon` struct and its attributes (#22002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - there's an extra newline ## Solution - remove the newline Co-authored-by: François Mockers --- crates/bevy_window/src/cursor/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_window/src/cursor/mod.rs b/crates/bevy_window/src/cursor/mod.rs index 257950d7f645f..bf9bd5c30449f 100644 --- a/crates/bevy_window/src/cursor/mod.rs +++ b/crates/bevy_window/src/cursor/mod.rs @@ -24,7 +24,6 @@ pub use crate::cursor::{CustomCursor, CustomCursorImage}; derive(Reflect), reflect(Component, Debug, Default, PartialEq, Clone) )] - pub enum CursorIcon { #[cfg(feature = "custom_cursor")] /// Custom cursor image. From cb719bed1f456801144dd0f369f10bca5739e510 Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sun, 7 Dec 2025 17:45:55 -0500 Subject: [PATCH 032/133] Update meshlet asset URL (#22058) Update URL after https://github.com/bevyengine/bevy_asset_files/pull/5 Fixes https://github.com/bevyengine/bevy/issues/21935 --- examples/3d/meshlet.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index e707de6ed8837..299df303cf263 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -12,7 +12,7 @@ use bevy::{ use std::f32::consts::PI; const ASSET_URL: &str = - "https://github.com/bevyengine/bevy_asset_files/raw/9bf88c42b9d06a3634eed633d90ce5fab02c31da/meshlet/bunny.meshlet_mesh"; + "https://github.com/bevyengine/bevy_asset_files/raw/6dccaef517bde74d1969734703709aead7211dbc/meshlet/bunny.meshlet_mesh"; fn main() { App::new() From 0235bdbf3cc1fbd08876a0bddb489b74c74bd3ae Mon Sep 17 00:00:00 2001 From: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Date: Sun, 7 Dec 2025 16:48:45 -0600 Subject: [PATCH 033/133] Replace `BTreeMap` with `IndexMap` for stored `ScheduleBuildPass`es (#21930) # Objective - Part of #20115 We currently store schedule build passes as a `BTreeMap`; however `TypeId` doesn't have a useful ordering for us, and is subject to change across compiles. ## Solution Switch to a `IndexMap`, so we can iterate in insertion order instead. ## Testing Added a test to ensure consistent iteration order. --- crates/bevy_ecs/src/schedule/schedule.rs | 70 ++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 3119f322cbfc0..bc636ecfd1490 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -4,19 +4,23 @@ )] use alloc::{ boxed::Box, - collections::{BTreeMap, BTreeSet}, + collections::BTreeSet, format, string::{String, ToString}, vec, vec::Vec, }; -use bevy_platform::collections::{HashMap, HashSet}; +use bevy_platform::{ + collections::{HashMap, HashSet}, + hash::FixedHasher, +}; use bevy_utils::{default, TypeIdMap}; use core::{ any::{Any, TypeId}, fmt::{Debug, Write}, }; use fixedbitset::FixedBitSet; +use indexmap::IndexMap; use log::{info, warn}; use pass::ScheduleBuildPassObj; use thiserror::Error; @@ -465,7 +469,7 @@ impl Schedule { /// Remove a custom build pass. pub fn remove_build_pass(&mut self) { - self.graph.passes.remove(&TypeId::of::()); + self.graph.passes.shift_remove(&TypeId::of::()); } /// Changes miscellaneous build settings. @@ -697,7 +701,7 @@ pub struct ScheduleGraph { anonymous_sets: usize, changed: bool, settings: ScheduleBuildSettings, - passes: BTreeMap>, + passes: IndexMap, FixedHasher>, } impl ScheduleGraph { @@ -1586,14 +1590,17 @@ pub struct ScheduleNotInitialized; #[cfg(test)] mod tests { + use alloc::{vec, vec::Vec}; + use core::any::TypeId; + use bevy_ecs_macros::ScheduleLabel; use crate::{ error::{ignore, panic, DefaultErrorHandler, Result}, prelude::{ApplyDeferred, IntoSystemSet, Res, Resource}, schedule::{ - tests::ResMut, IntoScheduleConfigs, Schedule, ScheduleBuildSettings, - ScheduleCleanupPolicy, SystemSet, + passes::AutoInsertApplyDeferredPass, tests::ResMut, IntoScheduleConfigs, Schedule, + ScheduleBuildPass, ScheduleBuildSettings, ScheduleCleanupPolicy, SystemSet, }, system::Commands, world::World, @@ -2571,4 +2578,55 @@ mod tests { let conflicts = schedule.graph().conflicting_systems(); assert!(conflicts.is_empty()); } + + #[test] + fn build_pass_iteration_order() { + #[derive(Debug)] + struct Pass; + + impl ScheduleBuildPass for Pass { + type EdgeOptions = (); + fn add_dependency( + &mut self, + _from: crate::schedule::NodeId, + _to: crate::schedule::NodeId, + _options: Option<&Self::EdgeOptions>, + ) { + } + fn build( + &mut self, + _world: &mut World, + _graph: &mut super::ScheduleGraph, + _dependency_flattened: &mut crate::schedule::graph::Dag, + ) -> core::result::Result<(), crate::schedule::ScheduleBuildError> { + Ok(()) + } + fn collapse_set( + &mut self, + _set: crate::schedule::SystemSetKey, + _systems: &bevy_platform::collections::HashSet, + _dependency_flattening: &crate::schedule::graph::DiGraph, + ) -> impl Iterator + { + core::iter::empty() + } + } + + let mut schedule = Schedule::default(); + schedule.add_build_pass(Pass::<0>); + schedule.add_build_pass(Pass::<1>); + schedule.add_build_pass(Pass::<2>); + + let pass_order: Vec = schedule.graph().passes.keys().cloned().collect(); + + assert_eq!( + pass_order, + vec![ + TypeId::of::(), + TypeId::of::>(), + TypeId::of::>(), + TypeId::of::>() + ] + ); + } } From d9c8330b1c56bab63dcce6b1ea5be5ebe4e02fbe Mon Sep 17 00:00:00 2001 From: Hennadii Chernyshchyk Date: Mon, 8 Dec 2025 00:52:35 +0200 Subject: [PATCH 034/133] Add `Observer::with_components` (#21857) # Objective - Closes #21848. ## Solution - Add `Observer::with_components`, for consistency with `Observer::with_entities` for entities. ## Testing - I didn't test it, but it's a very simple helper function, similar to already existing `Observer::with_component` (singular). --- crates/bevy_ecs/src/observer/distributed_storage.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs index f2af97d313d17..6f1ebebbc8cdc 100644 --- a/crates/bevy_ecs/src/observer/distributed_storage.rs +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -301,6 +301,13 @@ impl Observer { self } + /// Observes the given `components`. This will cause the [`Observer`] to run whenever the [`Event`] has + /// an [`EntityComponentsTrigger`](crate::event::EntityComponentsTrigger) that targets any of the `components`. + pub fn with_components>(mut self, components: I) -> Self { + self.descriptor.components.extend(components); + self + } + /// Observes the given `event_key`. This will cause the [`Observer`] to run whenever an event with the given [`EventKey`] /// is triggered. /// # Safety From 258a17561bac37940c5a808d651735f9ede14d0f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sun, 7 Dec 2025 22:53:44 +0000 Subject: [PATCH 035/133] Fix comment in `pointer_events` (#21996) # Objective Comment from `bevy_picking::events::pointer_events`: ``` // Emit Click and Up events on all the previously hovered entities. ``` The event is named `Release`, not `Up`. ## Solution Replace `Up` with `Release`. --- crates/bevy_picking/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 1385961e468d5..c418323bc01c5 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -632,7 +632,7 @@ pub fn pointer_events( PointerAction::Release(button) => { let state = pointer_state.get_mut(pointer_id, button); - // Emit Click and Up events on all the previously hovered entities. + // Emit Click and Release events on all the previously hovered entities. for (hovered_entity, hit) in previous_hover_map .get(&pointer_id) .iter() From d597657f28ce49d527a0c71f2a91266abebbf982 Mon Sep 17 00:00:00 2001 From: Tony <11652273+rmsthebest@users.noreply.github.com> Date: Mon, 8 Dec 2025 00:02:03 +0100 Subject: [PATCH 036/133] Slightly improve the window settings example (#21862) # Objective Update the example to avoid teaching new people that the recommended way of updating a component is to re-insert it. ## Solution Instead of inserting the component every time we want to change it, update the value. ## Testing - Did you test these changes? If so, how? Yes, ran the example - Are there any parts that need more testing? I don't think so? - How can other people (reviewers) test your changes? Is there anything specific they need to know? Run the example, press left and right mouse. - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? (linux/wayland) --- examples/window/window_settings.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/window/window_settings.rs b/examples/window/window_settings.rs index 6d64ca4754671..b02b7b7b06884 100644 --- a/examples/window/window_settings.rs +++ b/examples/window/window_settings.rs @@ -140,7 +140,7 @@ fn toggle_cursor(mut cursor_options: Single<&mut CursorOptions>, input: Res, input: Res>) { if input.just_pressed(KeyCode::KeyF) && let Some(current_theme) = window.window_theme @@ -152,14 +152,16 @@ fn toggle_theme(mut window: Single<&mut Window>, input: Res } } +/// Resource with a set of cursor icons we want to cycle through #[derive(Resource)] struct CursorIcons(Vec); fn init_cursor_icons( mut commands: Commands, + window: Single>, #[cfg(feature = "custom_cursor")] asset_server: Res, ) { - commands.insert_resource(CursorIcons(vec![ + let cursor_icons = CursorIcons(vec![ SystemCursorIcon::Default.into(), SystemCursorIcon::Pointer.into(), SystemCursorIcon::Wait.into(), @@ -171,30 +173,28 @@ fn init_cursor_icons( ..Default::default() }) .into(), - ])); + ]); + // By default the Window entity does not have a CursorIcon component, so we add it here. + commands.entity(*window).insert(cursor_icons.0[0].clone()); + commands.insert_resource(cursor_icons); } /// This system cycles the cursor's icon through a small set of icons when clicking fn cycle_cursor_icon( - mut commands: Commands, - window: Single>, + mut cursor: Single<&mut CursorIcon>, input: Res>, mut index: Local, cursor_icons: Res, ) { if input.just_pressed(MouseButton::Left) { *index = (*index + 1) % cursor_icons.0.len(); - commands - .entity(*window) - .insert(cursor_icons.0[*index].clone()); + **cursor = cursor_icons.0[*index].clone(); } else if input.just_pressed(MouseButton::Right) { *index = if *index == 0 { cursor_icons.0.len() - 1 } else { *index - 1 }; - commands - .entity(*window) - .insert(cursor_icons.0[*index].clone()); + **cursor = cursor_icons.0[*index].clone(); } } From e44d659a3f9426e12ab1db42f7f3ad0ee350b81b Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:30:14 -0500 Subject: [PATCH 037/133] Solari: Misc improvements (#21953) * Search for a spatial neighbor up to 5 times for ReSTIR instead of just once. * Bump RAY_T_MIN to 0.001. Prevents artifacts and doesn't really cause more self intersections. * Re-calculate world cache cell_size after jittering the query position to prevent creating extra cells at the wrong LOD for a given position. * Prevent the unlikely event of something checksumming to 0, which is reserved for empty cells. --- .../bevy_solari/src/realtime/restir_di.wgsl | 16 +++++++----- .../bevy_solari/src/realtime/restir_gi.wgsl | 26 +++++++++++-------- .../src/realtime/world_cache_query.wgsl | 5 ++-- .../src/scene/raytracing_scene_bindings.wgsl | 2 +- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/crates/bevy_solari/src/realtime/restir_di.wgsl b/crates/bevy_solari/src/realtime/restir_di.wgsl index b31f70f49963e..e7492f3c47ff3 100644 --- a/crates/bevy_solari/src/realtime/restir_di.wgsl +++ b/crates/bevy_solari/src/realtime/restir_di.wgsl @@ -179,15 +179,19 @@ fn load_temporal_reservoir_inner(temporal_pixel_id: vec2, depth: f32, world } fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { - let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); + for (var i = 0u; i < 5u; i++) { + let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); - let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); - let spatial_surface = gpixel_resolve(textureLoad(gbuffer, spatial_pixel_id, 0), spatial_depth, spatial_pixel_id, view.main_pass_viewport.zw, view.world_from_clip); - if pixel_dissimilar(depth, world_position, spatial_surface.world_position, world_normal, spatial_surface.world_normal, view) { - return empty_reservoir(); + let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); + let spatial_surface = gpixel_resolve(textureLoad(gbuffer, spatial_pixel_id, 0), spatial_depth, spatial_pixel_id, view.main_pass_viewport.zw, view.world_from_clip); + if pixel_dissimilar(depth, world_position, spatial_surface.world_position, world_normal, spatial_surface.world_normal, view) { + continue; + } + + return load_reservoir_b(spatial_pixel_id); } - return load_reservoir_b(spatial_pixel_id); + return empty_reservoir(); } fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index b0bbfa90ba421..a6355517af5c2 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -152,21 +152,25 @@ fn load_temporal_reservoir_inner(temporal_pixel_id: vec2, depth: f32, world } fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> NeighborInfo { - let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); + for (var i = 0u; i < 5u; i++) { + let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); - let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); - let spatial_surface = gpixel_resolve(textureLoad(gbuffer, spatial_pixel_id, 0), spatial_depth, spatial_pixel_id, view.main_pass_viewport.zw, view.world_from_clip); - let spatial_diffuse_brdf = spatial_surface.material.base_color / PI; - if pixel_dissimilar(depth, world_position, spatial_surface.world_position, world_normal, spatial_surface.world_normal, view) { - return NeighborInfo(empty_reservoir(), spatial_surface.world_position, spatial_surface.world_normal, spatial_diffuse_brdf); - } + let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); + let spatial_surface = gpixel_resolve(textureLoad(gbuffer, spatial_pixel_id, 0), spatial_depth, spatial_pixel_id, view.main_pass_viewport.zw, view.world_from_clip); + let spatial_diffuse_brdf = spatial_surface.material.base_color / PI; + if pixel_dissimilar(depth, world_position, spatial_surface.world_position, world_normal, spatial_surface.world_normal, view) { + continue; + } + + let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.main_pass_viewport.z); + var spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; - let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.main_pass_viewport.z); - var spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; + spatial_reservoir.radiance *= trace_point_visibility(world_position, spatial_reservoir.sample_point_world_position); - spatial_reservoir.radiance *= trace_point_visibility(world_position, spatial_reservoir.sample_point_world_position); + return NeighborInfo(spatial_reservoir, spatial_surface.world_position, spatial_surface.world_normal, spatial_diffuse_brdf); + } - return NeighborInfo(spatial_reservoir, spatial_surface.world_position, spatial_surface.world_normal, spatial_diffuse_brdf); + return NeighborInfo(empty_reservoir(), world_position, world_normal, vec3(0.0)); } fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { diff --git a/crates/bevy_solari/src/realtime/world_cache_query.wgsl b/crates/bevy_solari/src/realtime/world_cache_query.wgsl index 64a7d67fdd4ee..cafefffac58a8 100644 --- a/crates/bevy_solari/src/realtime/world_cache_query.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_query.wgsl @@ -47,12 +47,13 @@ struct WorldCacheGeometryData { #ifndef WORLD_CACHE_NON_ATOMIC_LIFE_BUFFER fn query_world_cache(world_position: vec3, world_normal: vec3, view_position: vec3, cell_lifetime: u32, rng: ptr) -> vec3 { - let cell_size = get_cell_size(world_position, view_position); + var cell_size = get_cell_size(world_position, view_position); // https://tomclabault.github.io/blog/2025/regir, jitter_world_position_tangent_plane let TBN = orthonormalize(world_normal); let offset = (rand_vec2f(rng) * 2.0 - 1.0) * cell_size * 0.5; let jittered_position = world_position + offset.x * TBN[0] + offset.y * TBN[1]; + cell_size = get_cell_size(jittered_position, view_position); let world_position_quantized = bitcast>(quantize_position(jittered_position, cell_size)); let world_normal_quantized = bitcast>(quantize_normal(world_normal)); @@ -121,7 +122,7 @@ fn compute_checksum(world_position: vec3, world_normal: vec3) -> u32 { key = iqint_hash(key + world_normal.x); key = iqint_hash(key + world_normal.y); key = iqint_hash(key + world_normal.z); - return key; + return max(key, 1u); // 0u is reserved for WORLD_CACHE_EMPTY_CELL } fn pcg_hash(input: u32) -> u32 { diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl index 8b1e875fc5423..4d5ec4584db33 100644 --- a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -83,7 +83,7 @@ const LIGHT_NOT_PRESENT_THIS_FRAME = 0xFFFFFFFFu; @group(0) @binding(10) var directional_lights: array; @group(0) @binding(11) var previous_frame_light_id_translations: array; -const RAY_T_MIN = 0.01f; +const RAY_T_MIN = 0.001f; const RAY_T_MAX = 100000.0f; const RAY_NO_CULL = 0xFFu; From 6c22c9b444ab376d2af8db6423c57e5eea57ac4c Mon Sep 17 00:00:00 2001 From: JMS55 <47158642+JMS55@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:32:42 -0500 Subject: [PATCH 038/133] Solari: Improve specular GI (#21907) Go back to only sampling the world cache for rough and non-first-bounce surfaces, but sample direct lighting when that doesn't happen. Mainline image This PR image Pathtraced reference image Fixes https://github.com/bevyengine/bevy/issues/21967. --- .../src/pathtracer/pathtracer.wgsl | 13 ++-- .../bevy_solari/src/realtime/specular_gi.wgsl | 69 +++++++++++++++---- crates/bevy_solari/src/scene/sampling.wgsl | 12 ++-- 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl index 77fc834b2f5a5..c2402deef7e40 100644 --- a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -4,7 +4,7 @@ #import bevy_render::maths::PI #import bevy_render::view::View #import bevy_solari::brdf::evaluate_brdf -#import bevy_solari::sampling::{sample_random_light, random_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, balance_heuristic, power_heuristic} +#import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, power_heuristic} #import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX} @group(1) @binding(0) var accumulation_texture: texture_storage_2d; @@ -48,7 +48,7 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { var mis_weight = 1.0; if !bounce_was_perfect_reflection { - let p_light = random_light_pdf(ray_hit); + let p_light = random_emissive_light_pdf(ray_hit); mis_weight = power_heuristic(p_bounce, p_light); } radiance += mis_weight * throughput * ray_hit.material.emissive; @@ -57,8 +57,13 @@ fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { let is_perfectly_specular = ray_hit.material.roughness <= 0.001 && ray_hit.material.metallic > 0.9999; if !is_perfectly_specular { let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); - let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit); - mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce); + + mis_weight = 1.0; + if direct_lighting.brdf_rays_can_hit { + let pdf_of_bounce = brdf_pdf(wo, direct_lighting.wi, ray_hit); + mis_weight = power_heuristic(1.0 / direct_lighting.inverse_pdf, pdf_of_bounce); + } + let direct_lighting_brdf = evaluate_brdf(ray_hit.world_normal, wo, direct_lighting.wi, ray_hit.material); radiance += mis_weight * throughput * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf; } diff --git a/crates/bevy_solari/src/realtime/specular_gi.wgsl b/crates/bevy_solari/src/realtime/specular_gi.wgsl index 335c5588271de..325cbdfda3a2d 100644 --- a/crates/bevy_solari/src/realtime/specular_gi.wgsl +++ b/crates/bevy_solari/src/realtime/specular_gi.wgsl @@ -3,8 +3,8 @@ #import bevy_render::view::View #import bevy_solari::brdf::{evaluate_brdf, evaluate_specular_brdf} #import bevy_solari::gbuffer_utils::gpixel_resolve -#import bevy_solari::sampling::{sample_ggx_vndf, ggx_vndf_pdf} -#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} +#import bevy_solari::sampling::{sample_random_light, random_emissive_light_pdf, sample_ggx_vndf, ggx_vndf_pdf, power_heuristic} +#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, ResolvedRayHitFull, RAY_T_MIN, RAY_T_MAX} #import bevy_solari::world_cache::{query_world_cache, WORLD_CACHE_CELL_LIFETIME} @group(1) @binding(0) var view_output: texture_storage_2d; @@ -15,6 +15,9 @@ struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; +const DIFFUSE_GI_REUSE_ROUGHNESS_THRESHOLD: f32 = 0.4; +const WORLD_CACHE_TERMINATION_ROUGHNESS_THRESHOLD: f32 = 0.4; + @compute @workgroup_size(8, 8, 1) fn specular_gi(@builtin(global_invocation_id) global_id: vec3) { if any(global_id.xy >= vec2u(view.main_pass_viewport.zw)) { return; } @@ -32,7 +35,7 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3) { var radiance: vec3; var wi: vec3; - if surface.material.roughness > 0.1 { + if surface.material.roughness > DIFFUSE_GI_REUSE_ROUGHNESS_THRESHOLD { // Surface is very rough, reuse the ReSTIR GI reservoir let gi_reservoir = gi_reservoirs_a[pixel_index]; wi = normalize(gi_reservoir.sample_point_world_position - surface.world_position); @@ -68,6 +71,8 @@ fn specular_gi(@builtin(global_invocation_id) global_id: vec3) { fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: ptr) -> vec3 { var ray_origin = initial_ray_origin; var wi = initial_wi; + var surface_perfectly_specular = false; + var p_bounce = 0.0; // Trace up to three bounces, getting the net throughput from them var radiance = vec3(0.0); @@ -78,34 +83,72 @@ fn trace_glossy_path(initial_ray_origin: vec3, initial_wi: vec3, rng: if ray.kind == RAY_QUERY_INTERSECTION_NONE { break; } let ray_hit = resolve_ray_hit_full(ray); - // Add world cache contribution - let diffuse_brdf = ray_hit.material.base_color / PI; - radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, rng); - - // Surface is very rough, terminate path in the world cache - if ray_hit.material.roughness > 0.1 && i != 0u { break; } - - // Sample new ray direction from the GGX BRDF for next bounce let TBN = calculate_tbn_mikktspace(ray_hit.world_normal, ray_hit.world_tangent); let T = TBN[0]; let B = TBN[1]; let N = TBN[2]; + let wo = -wi; let wo_tangent = vec3(dot(wo, T), dot(wo, B), dot(wo, N)); + + // Add emissive contribution (but not on the first bounce, since ReSTIR DI handles that) + if i != 0u { + radiance += throughput * emissive_mis_weight(p_bounce, ray_hit, surface_perfectly_specular) * ray_hit.material.emissive; + } + + // Should not perform NEE for mirror-like surfaces + surface_perfectly_specular = ray_hit.material.roughness <= 0.001 && ray_hit.material.metallic > 0.9999; + + if ray_hit.material.roughness > WORLD_CACHE_TERMINATION_ROUGHNESS_THRESHOLD && i != 0u { + // Surface is very rough, terminate path in the world cache + let diffuse_brdf = ray_hit.material.base_color / PI; + radiance += throughput * diffuse_brdf * query_world_cache(ray_hit.world_position, ray_hit.geometric_world_normal, view.world_position, WORLD_CACHE_CELL_LIFETIME, rng); + break; + } else if !surface_perfectly_specular { + // Sample direct lighting (NEE) + let direct_lighting = sample_random_light(ray_hit.world_position, ray_hit.world_normal, rng); + let direct_lighting_brdf = evaluate_brdf(ray_hit.world_normal, wo, direct_lighting.wi, ray_hit.material); + let mis_weight = nee_mis_weight(direct_lighting.inverse_pdf, direct_lighting.brdf_rays_can_hit, wo_tangent, direct_lighting.wi, ray_hit, TBN); + radiance += throughput * mis_weight * direct_lighting.radiance * direct_lighting.inverse_pdf * direct_lighting_brdf; + } + + // Sample new ray direction from the GGX BRDF for next bounce let wi_tangent = sample_ggx_vndf(wo_tangent, ray_hit.material.roughness, rng); wi = wi_tangent.x * T + wi_tangent.y * B + wi_tangent.z * N; ray_origin = ray_hit.world_position; // Update throughput for next bounce - let pdf = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); + p_bounce = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); let brdf = evaluate_brdf(N, wo, wi, ray_hit.material); let cos_theta = saturate(dot(wi, N)); - throughput *= (brdf * cos_theta) / pdf; + throughput *= (brdf * cos_theta) / p_bounce; } return radiance; } +fn emissive_mis_weight(p_bounce: f32, ray_hit: ResolvedRayHitFull, previous_surface_perfectly_specular: bool) -> f32 { + if previous_surface_perfectly_specular { return 1.0; } + + let p_light = random_emissive_light_pdf(ray_hit); + return power_heuristic(p_bounce, p_light); +} + +fn nee_mis_weight(inverse_p_light: f32, brdf_rays_can_hit: bool, wo_tangent: vec3, wi: vec3, ray_hit: ResolvedRayHitFull, TBN: mat3x3) -> f32 { + if !brdf_rays_can_hit { + return 1.0; + } + + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + let wi_tangent = vec3(dot(wi, T), dot(wi, B), dot(wi, N)); + + let p_light = 1.0 / inverse_p_light; + let p_bounce = ggx_vndf_pdf(wo_tangent, wi_tangent, ray_hit.material.roughness); + return max(0.0, power_heuristic(p_light, p_bounce)); +} + // Don't adjust the size of this struct without also adjusting GI_RESERVOIR_STRUCT_SIZE. struct Reservoir { sample_point_world_position: vec3, diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl index 8385f9b3999e6..b7fc2de2a0cc6 100644 --- a/crates/bevy_solari/src/scene/sampling.wgsl +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -15,6 +15,10 @@ fn balance_heuristic(f: f32, g: f32) -> f32 { // https://gpuopen.com/download/Bounded_VNDF_Sampling_for_Smith-GGX_Reflections.pdf (Listing 1) fn sample_ggx_vndf(wi_tangent: vec3, roughness: f32, rng: ptr) -> vec3 { + if roughness <= 0.01 { + return vec3(-wi_tangent.xy, wi_tangent.z); + } + let i = wi_tangent; let rand = rand_vec2f(rng); let i_std = normalize(vec3(i.xy * roughness, i.z)); @@ -69,6 +73,7 @@ struct LightContribution { radiance: vec3, inverse_pdf: f32, wi: vec3, + brdf_rays_can_hit: bool, } struct LightContributionNoPdf { @@ -88,10 +93,9 @@ fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rn return light_contribution; } -fn random_light_pdf(hit: ResolvedRayHitFull) -> f32 { +fn random_emissive_light_pdf(hit: ResolvedRayHitFull) -> f32 { let light_count = arrayLength(&light_sources); - let p_light = 1.0 / f32(light_count); - return p_light / (hit.triangle_area * f32(hit.triangle_count)); + return 1.0 / (f32(light_count) * f32(hit.triangle_count) * hit.triangle_area); } fn generate_random_light_sample(rng: ptr) -> GenerateRandomLightSampleResult { @@ -169,7 +173,7 @@ fn calculate_resolved_light_contribution(resolved_light_sample: ResolvedLightSam let radiance = resolved_light_sample.radiance * cos_theta_origin * (cos_theta_light / light_distance_squared); - return LightContribution(radiance, resolved_light_sample.inverse_pdf, wi); + return LightContribution(radiance, resolved_light_sample.inverse_pdf, wi, resolved_light_sample.world_position.w == 1.0); } fn resolve_and_calculate_light_contribution(light_sample: LightSample, ray_origin: vec3, origin_world_normal: vec3) -> LightContributionNoPdf { From 61e7b9a7bb5a5a42f053563ead1b8d732bc88076 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 7 Dec 2025 16:05:58 -0800 Subject: [PATCH 039/133] Increase the number of supported clustered decals if the `PARTIALLY_BOUND_BINDING_ARRAY` `wgpu` feature is present. (#22028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current clustered decal code predates `PARTIALLY_BOUND_BINDING_ARRAY` support in `wgpu`. As a result, only 8 clustered decals are allowed in the scene in order to avoid paying a high cost even if no decals of that type are present. This limitation makes Bevy's current clustered decal support insufficient for many use cases (e.g. bullet holes), so this patch increases the limit from 8 to 1024 if the `wgpu` features include `PARTIALLY_BOUND_BINDING_ARRAY`. --------- Co-authored-by: François Mockers --- crates/bevy_pbr/src/decal/clustered.rs | 45 +++++++++++++++++++------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index 51a141d15ae22..ad32c7f5715b8 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -38,6 +38,7 @@ use bevy_render::{ SamplerBindingType, ShaderType, TextureSampleType, TextureView, }, renderer::{RenderAdapter, RenderDevice, RenderQueue}, + settings::WgpuFeatures, sync_component::SyncComponentPlugin, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, @@ -49,13 +50,6 @@ use bytemuck::{Pod, Zeroable}; use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta}; -/// The maximum number of decals that can be present in a view. -/// -/// This number is currently relatively low in order to work around the lack of -/// first-class binding arrays in `wgpu`. When that feature is implemented, this -/// limit can be increased. -pub(crate) const MAX_VIEW_DECALS: usize = 8; - /// A plugin that adds support for clustered decals. /// /// In environments where bindless textures aren't available, clustered decals @@ -328,7 +322,7 @@ pub(crate) fn get_bind_group_layout_entries( binding_types::storage_buffer_read_only::(false), // `decal_textures` binding_types::texture_2d(TextureSampleType::Float { filterable: true }) - .count(NonZero::::new(MAX_VIEW_DECALS as u32).unwrap()), + .count(NonZero::::new(max_view_decals(render_device)).unwrap()), // `decal_sampler` binding_types::sampler(SamplerBindingType::Filtering), ]) @@ -372,10 +366,16 @@ impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> { } } - // Pad out the binding array to its maximum length, which is - // required on some platforms. - while texture_views.len() < MAX_VIEW_DECALS { - texture_views.push(&*fallback_image.d2.texture_view); + // If required on this platform, pad out the binding array to its + // maximum length. + if !render_device + .features() + .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) + { + let max_view_decals = max_view_decals(render_device); + while texture_views.len() < max_view_decals as usize { + texture_views.push(&*fallback_image.d2.texture_view); + } } Some(RenderViewClusteredDecalBindGroupEntries { @@ -439,3 +439,24 @@ pub fn clustered_decals_are_usable( binding_arrays_are_usable(render_device, render_adapter) && cfg!(feature = "pbr_clustered_decals") } + +/// Returns the maximum number of decals that can be in the scene, taking +/// platform limitations into account. +fn max_view_decals(render_device: &RenderDevice) -> u32 { + // If the current `wgpu` platform doesn't support partially-bound binding + // arrays, limit the number of decals to a low number. If we didn't do this, + // then on such platforms we'd pay the maximum overhead even if there are no + // decals are in the scene. + if render_device + .features() + .contains(WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY) + { + // This number was determined arbitrarily as a reasonable value that + // would encompass most use cases (e.g. bullet holes in walls) while + // offering a failsafe to prevent shaders becoming too slow if there are + // extremely large numbers of decals. + 1024 + } else { + 8 + } +} From 2403c41da483cf6e1c663dd9705b7d7f3cc04a44 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 7 Dec 2025 23:55:23 -0500 Subject: [PATCH 040/133] Fix flaky asset test, adds todo for a real fix (#22061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Unblock pull requests and merges from the failings of this flaky asset test - Fixes the flakiness described in #22001 ## Solution Change the assertion to accept both 2 and 3. Add an explaination why, and add a todo to resolve the underlying issue. FYI @andriyDev since I don’t yet feel comfortable messing with the locks, just figured I’d throw this up to reduce confusion about failing builds in the meantime --- crates/bevy_asset/src/processor/tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_asset/src/processor/tests.rs b/crates/bevy_asset/src/processor/tests.rs index b31abb47bba02..c1c5f56d82e33 100644 --- a/crates/bevy_asset/src/processor/tests.rs +++ b/crates/bevy_asset/src/processor/tests.rs @@ -1495,10 +1495,11 @@ fn only_reprocesses_wrong_hash_on_startup() { // Only source_changed and dep_changed assets were reprocessed - all others still have the same // hashes. - assert_eq!( - *transformer.0.lock().unwrap_or_else(PoisonError::into_inner), - 2 - ); + let num_processes = *transformer.0.lock().unwrap_or_else(PoisonError::into_inner); + // TODO: assert_eq! (num_processes == 2) only after we prevent double processing assets + // == 3 happens when the initial processing of an asset and the re-processing that its dependency + // triggers are both able to proceed. (dep_changed_asset in this case is processed twice) + assert!(num_processes == 2 || num_processes == 3); assert_eq!( read_asset_as_string(&default_processed_dir, no_deps_asset), From 9ad7df93c6324b4d8be9264c15f5921cf5fe6be4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:37:45 +0100 Subject: [PATCH 041/133] Update ron requirement from 0.11 to 0.12 (#22063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [ron](https://github.com/ron-rs/ron) to permit the latest version.
Release notes

Sourced from ron's releases.

v0.12.0

What's Changed

New Contributors

Full Changelog: https://github.com/ron-rs/ron/compare/v0.11.0...v0.12.0

Changelog

Sourced from ron's changelog.

[0.12.0] - 2025-11-12

API Changes

  • Breaking: Removed the ron::error::Error::Base64Error variant. (#566)
  • Added into_inner() method to ron::ser::Serializer to retrieve the inner writer. (#588)
  • Removed the base64 dependency. (#566)

Format Changes

  • Format-Breaking: Remote base64-encoded byte strings deserialisation, replaced by Rusty byte strings in v0.9.0 (#566)

Bug Fixes

  • Fixed untagged enum deserialisation for serde >= 1.0.220 with better serde content detection (#582)

[0.11.0] - 2025-08-27

API Changes

  • Breaking: SpannedError now stores the full error span in span: Span { start: Position, end: Position }`, to facilitate, e.g., language server highlighting of syntax errors.
  • Breaking: Added no_std support via a new std feature (enabled by default). With default features disabled, you must enable the std feature to access de::from_reader, and the std::io operations on Options, such as from_reader, from_reader_seed, to_io_writer, and to_io_writer_pretty (#567)
  • Breaking: Fixed (again) ron::value::Number to ensure it is non-exhaustive, to avoid breaking matches when feature unification enables more of its variants than expected (#568)

Examples

  • Add a new example file_read_write_vec.rs for reading and writing Vec<T> to/from files. (#573)

[0.10.1] - 2025-04-08

API Changes

  • Add ron::Options::to_io_writer and ron::Options::to_io_writer_pretty to allow writing into an io::Writer (#561)
  • Breaking: ron::value::Number is now non-exhaustive, to avoid breaking matches when feature unification enables more of its variants than expected (#564)

[0.9.0] - 2025-03-18

API Changes

  • Add ron::value::RawValue helper type which can (de)serialize any valid RON (#407)
  • Add escape_strings option to PrettyConfig to allow serialising with or without escaping (#426)
  • Add compact_maps and compact_structs options to PrettyConfig to allow serialising maps and structs on a single line (#448)
  • Add minimal support for #[serde(flatten)] with roundtripping through RON maps (#455)
  • Add minimal roundtripping support for #[serde(tag = "tag")], #[serde(tag = "tag", content = "content")], and #[serde(untagged)] enums (#451)
  • Breaking: Expand the value::Number enum to explicitly encode all possible number types (#479)
  • Add number_suffixes option to PrettyConfig to allow serialising numbers with their explicit type suffix, e.g. 42i32 (#481)
  • Allow ron::value::RawValue to capture any whitespace to the left and right of a ron value (#487)
  • Breaking: Enforce that ron always writes valid UTF-8 (#488)
  • Add convenient Value::from impls (#498)
  • Add new extension explicit_struct_names which requires that struct names are included during deserialization (#522)

... (truncated)

Commits
  • c6a8cff Bump ron to v0.12.0 (#591)
  • f2c1728 v0.12: Further deprecate base64 byte strings by removing parsing support outs...
  • 5630b0f Fixed serde content detection for serde >= 1.0.220 (#582)
  • 3184df3 Add ron-lsp to README (#589)
  • 9b9b88e Add Serializer::into_inner method to extract the writer (#588)
  • See full diff in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- crates/bevy_animation/Cargo.toml | 2 +- crates/bevy_asset/Cargo.toml | 2 +- crates/bevy_dev_tools/Cargo.toml | 2 +- crates/bevy_reflect/Cargo.toml | 2 +- crates/bevy_scene/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa3982b0c3a12..4a1143d51cee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -708,7 +708,7 @@ bevy_dylib = { path = "crates/bevy_dylib", version = "0.18.0-dev", default-featu [dev-dependencies] rand = "0.9.0" rand_chacha = "0.9.0" -ron = "0.11" +ron = "0.12" flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 27338adbb444a..047609fd84f07 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -33,7 +33,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev", default-fea # other petgraph = { version = "0.8", features = ["serde-1"] } -ron = "0.11" +ron = "0.12" serde = "1" blake3 = { version = "1.0" } downcast-rs = { version = "2", default-features = false, features = ["std"] } diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 39a363433dfbf..d20e9c0974ee7 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -59,7 +59,7 @@ futures-util = { version = "0.3", default-features = false, features = [ futures-io = { version = "0.3", default-features = false } futures-lite = { version = "2.0.1", default-features = false } blake3 = { version = "1.5", default-features = false } -ron = { version = "0.11", default-features = false } +ron = { version = "0.12", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "2", default-features = false, features = ["from"] } diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index e3e063cdcfc79..9d8567af0a6eb 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -34,7 +34,7 @@ bevy_state = { path = "../bevy_state", version = "0.18.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } -ron = { version = "0.11", optional = true } +ron = { version = "0.12", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 2308719106112..5d5dfe1bc3249 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -133,7 +133,7 @@ wgpu-types = { version = "26", features = [ inventory = { version = "0.3", optional = true } [dev-dependencies] -ron = "0.11" +ron = "0.12" rmp-serde = "1.1" bincode = { version = "2.0", features = ["serde"] } serde_json = "1.0.140" diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index f61bfd2357093..fe716124d8774 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -33,7 +33,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.18.0-dev", default-fea ] } # other -ron = { version = "0.11", default-features = false, optional = true } +ron = { version = "0.12", default-features = false, optional = true } serde = { version = "1.0", features = ["derive"], optional = true } uuid = { version = "1.13.1", features = ["v4"] } thiserror = { version = "2", default-features = false } From 2144c03adadc30fd1992862617dc25a949bb83b9 Mon Sep 17 00:00:00 2001 From: Joda Date: Mon, 8 Dec 2025 05:54:37 -0600 Subject: [PATCH 042/133] Prevent WebGL builds from crashing on startup when using FpsOverlayPlugin (#21885) # Objective - Fixes #21362 - The FrameTimeGraph uses a material that crashes in WebGL (but not WebGPU) even if the FrameTimeGraph is disabled. We need to prevent the FrameTimeGraph from being created when we build for WASM. ## Solution - If the build is for WASM with only WebGL, we skip the ceration of the FrameTimeGraph. Additionally, if the FrameTimeGraph is enabled, we provide a warning to inform the user that the FrameTimeGraph isn't supported on WebGL. ## Testing - I used the repro provided in the issue. --- crates/bevy_dev_tools/Cargo.toml | 2 + crates/bevy_dev_tools/src/fps_overlay.rs | 67 ++++++++++++++---------- crates/bevy_internal/Cargo.toml | 2 + 3 files changed, 43 insertions(+), 28 deletions(-) diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 9d8567af0a6eb..dc218f023baef 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -10,6 +10,8 @@ keywords = ["bevy"] [features] bevy_ci_testing = ["serde", "ron"] +webgl = ["bevy_render/webgl"] +webgpu = ["bevy_render/webgpu"] [dependencies] # bevy diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index f180b6ef9e810..f6a3c10269856 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -188,35 +188,46 @@ fn setup( )) .with_child((TextSpan::default(), overlay_config.text_config.clone())); - let font_size = overlay_config.text_config.font_size; - p.spawn(( - Node { - width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE), - height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE), - display: if overlay_config.frame_time_graph_config.enabled { - bevy_ui::Display::DEFAULT - } else { - bevy_ui::Display::None - }, - ..Default::default() - }, - Pickable::IGNORE, - MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial { - values: buffers.add(ShaderStorageBuffer { - // Initialize with dummy data because the default (`data: None`) will - // cause a panic in the shader if the frame time graph is constructed - // with `enabled: false`. - data: Some(vec![0, 0, 0, 0]), + #[cfg(all(target_arch = "wasm32", not(feature = "webgpu")))] + { + if overlay_config.frame_time_graph_config.enabled { + use tracing::warn; + + warn!("Frame time graph is not supported with WebGL. Consider if WebGPU is viable for your usecase."); + } + } + #[cfg(not(all(target_arch = "wasm32", not(feature = "webgpu"))))] + { + let font_size = overlay_config.text_config.font_size; + p.spawn(( + Node { + width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE), + height: Val::Px(font_size * FRAME_TIME_GRAPH_HEIGHT_SCALE), + display: if overlay_config.frame_time_graph_config.enabled { + bevy_ui::Display::DEFAULT + } else { + bevy_ui::Display::None + }, ..Default::default() - }), - config: FrameTimeGraphConfigUniform::new( - overlay_config.frame_time_graph_config.target_fps, - overlay_config.frame_time_graph_config.min_fps, - true, - ), - })), - FrameTimeGraph, - )); + }, + Pickable::IGNORE, + MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial { + values: buffers.add(ShaderStorageBuffer { + // Initialize with dummy data because the default (`data: None`) will + // cause a panic in the shader if the frame time graph is constructed + // with `enabled: false`. + data: Some(vec![0, 0, 0, 0]), + ..Default::default() + }), + config: FrameTimeGraphConfigUniform::new( + overlay_config.frame_time_graph_config.target_fps, + overlay_config.frame_time_graph_config.min_fps, + true, + ), + })), + FrameTimeGraph, + )); + } }); } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index f6872757b04a7..c894e3664405f 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -187,6 +187,7 @@ webgl = [ "bevy_render?/webgl", "bevy_gizmos_render?/webgl", "bevy_sprite_render?/webgl", + "bevy_dev_tools?/webgl", ] webgpu = [ @@ -196,6 +197,7 @@ webgpu = [ "bevy_render?/webgpu", "bevy_gizmos_render?/webgpu", "bevy_sprite_render?/webgpu", + "bevy_dev_tools?/webgpu", ] # Enable systems that allow for automated testing on CI From d6ae071771e62b5e896752bd5ed1a3e29249291e Mon Sep 17 00:00:00 2001 From: Chris Biscardi Date: Mon, 8 Dec 2025 03:55:03 -0800 Subject: [PATCH 043/133] anchor layout example (#21225) # Objective There was [some discussion in discord](https://discord.com/channels/691052431525675048/1264881140007702558/1420909052879175810) about "anchor layouts". They are, as far as I can tell, basically absolute positioning. ## Solution Write an example that shows how to "anchor" elements to the top/left/right/bottom of a box. Also optionally included "centering" using auto margins. ## Testing ``` cargo run --example anchor_positioning ``` Moving the window around changes the gray box sizes, while the interior labels are "anchored" to their respective positions, offset from their specific side attachments by 10 pixels ## Showcase screenshot-2025-09-26-at-02 17
31@2x --------- Co-authored-by: ickshonpe --- Cargo.toml | 11 +++ examples/README.md | 1 + examples/ui/anchor_layout.rs | 146 +++++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 examples/ui/anchor_layout.rs diff --git a/Cargo.toml b/Cargo.toml index 4a1143d51cee7..d0b21ce74bbe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3414,6 +3414,17 @@ category = "Transforms" wasm = true # UI (User Interface) +[[example]] +name = "anchor_layout" +path = "examples/ui/anchor_layout.rs" +doc-scrape-examples = true + +[package.metadata.example.anchor_layout] +name = "Anchor Layout" +description = "Shows an 'anchor layout' style of ui layout" +category = "UI (User Interface)" +wasm = true + [[example]] name = "borders" path = "examples/ui/borders.rs" diff --git a/examples/README.md b/examples/README.md index 440c54d0c2bdb..ab7402ee46a9c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -553,6 +553,7 @@ Example | Description Example | Description --- | --- +[Anchor Layout](../examples/ui/anchor_layout.rs) | Shows an 'anchor layout' style of ui layout [Automatic Directional Navigation](../examples/ui/auto_directional_navigation.rs) | Demonstration of automatic directional navigation graph generation based on UI element positions [Borders](../examples/ui/borders.rs) | Demonstrates how to create a node with a border [Box Shadow](../examples/ui/box_shadow.rs) | Demonstrates how to create a node with a shadow diff --git a/examples/ui/anchor_layout.rs b/examples/ui/anchor_layout.rs new file mode 100644 index 0000000000000..6509a7790a854 --- /dev/null +++ b/examples/ui/anchor_layout.rs @@ -0,0 +1,146 @@ +//! Shows an "anchor layout" style of ui layout +use bevy::prelude::*; + +const MARGIN: Val = Val::Px(12.); + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Anchor Layout Example".to_string(), + ..default() + }), + ..default() + })) + .add_systems(Startup, spawn_layout) + .run(); +} + +fn spawn_layout(mut commands: Commands, asset_server: Res) { + let font = asset_server.load("fonts/FiraSans-Bold.ttf"); + commands.spawn(Camera2d); + + let rows = [ + ( + "left: 10px\ntop: 10px", + Node { + left: px(10), + top: px(10), + ..default() + }, + ), + ( + "center: 10px\ntop: 10px", + Node { + margin: auto().horizontal(), + top: px(10), + ..default() + }, + ), + ( + "right: 10px\ntop: 10px", + Node { + right: px(10), + top: px(10), + ..default() + }, + ), + ( + "left: 10px\ncenter: 10px", + Node { + left: px(10), + margin: UiRect::vertical(Val::Auto), + ..default() + }, + ), + ( + "center: 10px\ncenter: 10px", + Node { + margin: UiRect::all(Val::Auto), + ..default() + }, + ), + ( + "right: 10px\ncenter: 10px", + Node { + right: px(10), + margin: UiRect::vertical(Val::Auto), + ..default() + }, + ), + ( + "left: 10px\nbottom: 10px", + Node { + left: px(10), + bottom: px(10), + ..default() + }, + ), + ( + "center: 10px\nbottom: 10px", + Node { + margin: UiRect::horizontal(Val::Auto), + bottom: px(10), + ..default() + }, + ), + ( + "right: 10px\nbottom: 10px", + Node { + right: px(10), + bottom: px(10), + ..default() + }, + ), + ]; + + // let font = font.clone(); + commands.spawn(( + Node { + // fill the entire window + width: percent(100), + height: percent(100), + padding: MARGIN.all(), + row_gap: MARGIN, + column_gap: MARGIN, + display: Display::Grid, + grid_template_columns: RepeatedGridTrack::fr(3, 1.), + grid_template_rows: RepeatedGridTrack::fr(3, 1.), + ..default() + }, + BackgroundColor(Color::BLACK), + Children::spawn(SpawnIter( + rows.into_iter() + .map(move |v| anchored_node(font.clone(), v.1, v.0)), + )), + )); +} + +fn anchored_node(font: Handle, node: Node, label: &str) -> impl Bundle { + ( + // outer gray box + Node { + grid_column: GridPlacement::span(1), + grid_row: GridPlacement::span(1), + ..default() + }, + BackgroundColor(Color::srgb(0.25, 0.25, 0.25)), + children![ + // inner label box + ( + Node { + display: Display::Block, + padding: UiRect::axes(px(5), px(1)), + position_type: PositionType::Absolute, + ..node + }, + BackgroundColor(Color::srgb(1., 0.066, 0.349)), + children![( + Text::new(label), + TextFont { font, ..default() }, + TextColor::BLACK, + )], + ) + ], + ) +} From d13f60739e67fd032209429df0518b465b94f3ab Mon Sep 17 00:00:00 2001 From: natepiano Date: Mon, 8 Dec 2025 16:09:05 -0500 Subject: [PATCH 044/133] Fix macOS panic when exiting from exclusive fullscreen (#22060) # Objective Fix panic on macOS when exiting an app while in exclusive fullscreen mode. ## Solution Drop windows from `WINIT_WINDOWS` TLS in `fn exiting` before the event loop returns. The panic occurs because: 1. `WINIT_WINDOWS` is stored in TLS 2. Windows are dropped during TLS destruction (after event loop exits) 3. winit's `Window::drop` calls `set_fullscreen(None)` for exclusive fullscreen 4. macOS sends a callback that tries to access TLS already being destroyed By clearing windows in `fn exiting`, they're dropped while the event loop is still active, avoiding the TLS access issue. ## Testing - Tested on macOS 26.1 (Sequoia) with M2 Max - Run `cargo run --example monitor_info`, then Cmd+Q to quit - Without fix: panic with "cannot access a Thread Local Storage value during or after destruction" - With fix: clean exit - Windows is unaffected --------- Co-authored-by: Alice Cecile --- crates/bevy_winit/src/state.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 08c9e36535bfa..9e347651f57e4 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -496,6 +496,10 @@ impl ApplicationHandler for WinitAppRunnerState { } fn exiting(&mut self, _event_loop: &ActiveEventLoop) { + // Drop windows while event loop is still active, before TLS destruction. + // Prevents panic on macOS when exiting from exclusive fullscreen. + WINIT_WINDOWS.with(|ww| ww.borrow_mut().windows.clear()); + let world = self.world_mut(); world.clear_all(); } From d5dfab9361ac8a0f843ec0a089e557e119f034f9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 8 Dec 2025 21:29:40 +0000 Subject: [PATCH 045/133] UI material z-offset override (#20279) # Objective In #20237 the swatch implementation uses a child node to display the foreground color because UI materials have a higher local z offset than everything else. ## Solution Add a `stack_z_offset` function to the `UiMaterial` trait that can be overriden to set a custom z-offset. --- crates/bevy_ui_render/src/ui_material.rs | 4 ++++ crates/bevy_ui_render/src/ui_material_pipeline.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/ui_material.rs b/crates/bevy_ui_render/src/ui_material.rs index fc3ff836bdc40..54610e5f2a44c 100644 --- a/crates/bevy_ui_render/src/ui_material.rs +++ b/crates/bevy_ui_render/src/ui_material.rs @@ -112,6 +112,10 @@ pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized { ShaderRef::Default } + fn stack_z_offset() -> f32 { + crate::stack_z_offsets::MATERIAL + } + #[expect( unused_variables, reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion." diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 7c32b90918805..5398dba2f08f7 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -636,7 +636,7 @@ pub fn queue_ui_material_nodes( draw_function, pipeline, entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::MATERIAL), + sort_key: FloatOrd(extracted_uinode.stack_index as f32 + M::stack_z_offset()), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, index, From 0d13444db8c251e4f14444188c1e6975536d52f7 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:34:11 +0000 Subject: [PATCH 046/133] clear animation evaluator stack (#21118) # Objective A large complex animation (2642 curves over 62 joints) causes serious residual noise on joints after it stops playing. here i start the animation then quickly cancel it: https://github.com/user-attachments/assets/9552d69e-bb10-400f-94a6-c93d301c689b this also seems to affect entities that are animated, but not by the player that fires the large animation, which is really bizarre (here i do not cancel it): https://github.com/user-attachments/assets/a0d99df5-f53d-4ff0-9cf1-87a0e78b8abd ## Solution debugging i found that the curve evaluator stack is growing unbounded while the complex animation is playing (100k items after a second or two) and is not cleared after the animation is stopped / replaced, so i tried clearing the evaluator stack and that seems to fix it. i have no idea if this fix is sensible or if the root of the issue is elsewhere. i don't see any knockon ill-effects but can't be sure there aren't any. i do not understand this code at all and it's hard to get a clear picture of what it's trying to do, so hopefully somebody who does understand it can diagnose better. i attached the gltf containing the animation that causes the issue but i can't give a simple repro - in my code i have to retarget the animation onto an avatar which isn't included in the animation gltf (i can provide an example for that if anybody wants it). [bafybeicoja22tkr2rxpccmwh74yehnurgolias77reztd2kuhtvsn2luue.glb.zip](https://github.com/user-attachments/files/22408365/bafybeicoja22tkr2rxpccmwh74yehnurgolias77reztd2kuhtvsn2luue.glb.zip) ## Testing no Co-authored-by: Alice Cecile --- crates/bevy_animation/src/animation_curves.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index a9468c0742d01..4bea4384f7cac 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -416,6 +416,7 @@ impl AnimationCurveEvaluator for AnimatableCurveEvaluator { .pop() .ok_or_else(inconsistent::>)? .value; + self.evaluator.stack.clear(); Ok(()) } } From 0af6fc1c747c9df2b5b8c8b599c955e61bd10e44 Mon Sep 17 00:00:00 2001 From: Christophe Bronner Date: Mon, 8 Dec 2025 16:37:20 -0500 Subject: [PATCH 047/133] Implement world.trigger_event remote method (#21798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Tools using `bevy_remote` will be able to identify (via the schema) and trigger events. - [x] Document `bevy_ecs/src/reflect/event.rs` ## Solution I've added a method `world.trigger_event`, added `Event` to the schema's reflection metadata and `ReflectEvent` to allow this. ## Testing I have copied the (tested) code from my game but have NOT tested this branch yet. I am new to Rust/Cargo and need to go to sleep now, I'll figure this out and test it tomorrow. --- ## Showcase Here's what I needed to add to my game in order to allow my editor to access and trigger an event: ```rust #[derive(Event, Reflect)] #[reflect(Event)] pub struct AssignToRoute { pub vehicle: Entity, pub route: Entity, pub origin: Entity, } ``` Here's a screenshot of my editor using this feature: Screenshot 2025-11-10 at 1 40 42 AM --- crates/bevy_ecs/src/lib.rs | 2 +- crates/bevy_ecs/src/reflect/event.rs | 76 ++++++++++++++++++ crates/bevy_ecs/src/reflect/mod.rs | 2 + crates/bevy_remote/src/builtin_methods.rs | 98 ++++++++++++++++++++++- crates/bevy_remote/src/lib.rs | 16 +++- crates/bevy_remote/src/schemas/mod.rs | 3 +- 6 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 crates/bevy_ecs/src/reflect/event.rs diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index b8835257a66dd..4c8763ad66f64 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -109,7 +109,7 @@ pub mod prelude { #[doc(hidden)] #[cfg(feature = "bevy_reflect")] pub use crate::reflect::{ - AppTypeRegistry, ReflectComponent, ReflectFromWorld, ReflectResource, + AppTypeRegistry, ReflectComponent, ReflectEvent, ReflectFromWorld, ReflectResource, }; #[doc(hidden)] diff --git a/crates/bevy_ecs/src/reflect/event.rs b/crates/bevy_ecs/src/reflect/event.rs new file mode 100644 index 0000000000000..7ae2bcde8481a --- /dev/null +++ b/crates/bevy_ecs/src/reflect/event.rs @@ -0,0 +1,76 @@ +//! Definitions for [`Event`] reflection. +//! This allows triggering events whose type is only known at runtime. +//! +//! This module exports two types: [`ReflectEventFns`] and [`ReflectEvent`]. +//! +//! Same as [`component`](`super::component`), but for events. + +use crate::{event::Event, reflect::from_reflect_with_fallback, world::World}; +use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry}; + +/// A struct used to operate on reflected [`Event`] trait of a type. +/// +/// A [`ReflectEvent`] for type `T` can be obtained via +/// [`bevy_reflect::TypeRegistration::data`]. +#[derive(Clone)] +pub struct ReflectEvent(ReflectEventFns); + +/// The raw function pointers needed to make up a [`ReflectEvent`]. +/// +/// This is used when creating custom implementations of [`ReflectEvent`] with +/// [`ReflectEventFns::new()`]. +/// +/// > **Note:** +/// > Creating custom implementations of [`ReflectEvent`] is an advanced feature that most users +/// > will not need. +/// > Usually a [`ReflectEvent`] is created for a type by deriving [`Reflect`] +/// > and adding the `#[reflect(Event)]` attribute. +/// > After adding the component to the [`TypeRegistry`], +/// > its [`ReflectEvent`] can then be retrieved when needed. +/// +/// Creating a custom [`ReflectEvent`] may be useful if you need to create new component types +/// at runtime, for example, for scripting implementations. +/// +/// By creating a custom [`ReflectEvent`] and inserting it into a type's +/// [`TypeRegistration`][bevy_reflect::TypeRegistration], +/// you can modify the way that reflected event of that type will be triggered in the Bevy +/// world. +#[derive(Clone)] +pub struct ReflectEventFns { + trigger: fn(&mut World, &dyn PartialReflect, &TypeRegistry), +} + +impl ReflectEventFns { + /// Get the default set of [`ReflectEventFns`] for a specific event 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<'a, T: Event + FromReflect + TypePath>() -> Self + where + T::Trigger<'a>: Default, + { + >::from_type().0 + } +} + +impl ReflectEvent { + /// Triggers a reflected [`Event`] like [`trigger()`](World::trigger). + pub fn trigger(&self, world: &mut World, event: &dyn PartialReflect, registry: &TypeRegistry) { + (self.0.trigger)(world, event, registry); + } +} + +impl<'a, E: Event + Reflect + TypePath> FromType for ReflectEvent +where + ::Trigger<'a>: Default, +{ + fn from_type() -> Self { + ReflectEvent(ReflectEventFns { + trigger: |world, reflected_event, registry| { + let event = from_reflect_with_fallback::(reflected_event, world, registry); + world.trigger(event); + }, + }) + } +} diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index c306723f3a707..c0c5ee7ade282 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -14,6 +14,7 @@ use bevy_reflect::{ mod bundle; mod component; mod entity_commands; +mod event; mod from_world; mod map_entities; mod resource; @@ -22,6 +23,7 @@ use bevy_utils::prelude::DebugName; pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use entity_commands::ReflectCommandExt; +pub use event::{ReflectEvent, ReflectEventFns}; pub use from_world::{ReflectFromWorld, ReflectFromWorldFns}; pub use map_entities::ReflectMapEntities; pub use resource::{ReflectResource, ReflectResourceFns}; diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index abe8881731fc0..5c359dab5b2e1 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -10,17 +10,17 @@ use bevy_ecs::{ lifecycle::RemovedComponentEntity, message::MessageCursor, query::QueryBuilder, - reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, + reflect::{AppTypeRegistry, ReflectComponent, ReflectEvent, ReflectResource}, system::{In, Local}, - world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, + world::{EntityRef, EntityWorldMut, FilteredEntityRef, Mut, World}, }; use bevy_log::warn_once; use bevy_platform::collections::HashMap; use bevy_reflect::{ serde::{ReflectSerializer, TypedReflectDeserializer}, - GetPath, PartialReflect, TypeRegistration, TypeRegistry, + DynamicStruct, GetPath, PartialReflect, TypeRegistration, TypeRegistry, }; -use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; +use serde::{de::DeserializeSeed as _, de::IntoDeserializer, Deserialize, Serialize}; use serde_json::{Map, Value}; use crate::{ @@ -83,6 +83,9 @@ pub const BRP_MUTATE_RESOURCE_METHOD: &str = "world.mutate_resources"; /// The method path for a `world.list_resources` request. pub const BRP_LIST_RESOURCES_METHOD: &str = "world.list_resources"; +/// The method path for a `world.trigger_event` request. +pub const BRP_TRIGGER_EVENT_METHOD: &str = "world.trigger_event"; + /// The method path for a `registry.schema` request. pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "registry.schema"; @@ -299,6 +302,19 @@ pub struct BrpMutateResourcesParams { pub value: Value, } +/// `world.trigger_event`: +/// +/// The server responds with a null. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +struct BrpTriggerEventParams { + /// The [full path] of the event to trigger. + /// + /// [full path]: bevy_reflect::TypePath::type_path + pub event: String, + /// The serialized value of the event to be triggered, if any. + pub value: Option, +} + /// Describes the data that is to be fetched in a query. #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] pub struct BrpQuery { @@ -1348,6 +1364,44 @@ pub fn process_remote_list_components_watching_request( } } +/// Handles a `world.trigger_event` request coming from a client. +pub fn process_remote_trigger_event_request( + In(params): In>, + world: &mut World, +) -> BrpResult { + let BrpTriggerEventParams { event, value } = parse_some(params)?; + + world.resource_scope(|world, registry: Mut| { + let registry = registry.read(); + + let Some(registration) = registry.get_with_type_path(&event) else { + return Err(BrpError::resource_error(format!( + "Unknown event type: `{event}`" + ))); + }; + let Some(reflect_event) = registration.data::() else { + return Err(BrpError::resource_error(format!( + "Event `{event}` is not reflectable" + ))); + }; + + if let Some(payload) = value { + let payload: Box = + TypedReflectDeserializer::new(registration, ®istry) + .deserialize(payload.into_deserializer()) + .map_err(|err| { + BrpError::resource_error(format!("{event} is invalid: {err}")) + })?; + reflect_event.trigger(world, &*payload, ®istry); + } else { + let payload = DynamicStruct::default(); + reflect_event.trigger(world, &payload, ®istry); + } + + Ok(Value::Null) + }) +} + /// Handles a `registry.schema` request (list all registry types in form of schema) coming from a client. pub fn export_registry_types(In(params): In>, world: &World) -> BrpResult { let filter: BrpJsonSchemaQueryFilter = match params { @@ -1628,6 +1682,11 @@ mod tests { } use super::*; + use bevy_ecs::{ + component::Component, event::Event, observer::On, resource::Resource, system::ResMut, + }; + use bevy_reflect::Reflect; + use serde_json::Value::Null; #[test] fn insert_reflect_only_component() { @@ -1659,6 +1718,37 @@ mod tests { insert_reflected_components(e, deserialized_components).expect("FAIL"); } + #[test] + fn trigger_reflect_only_event() { + #[derive(Event, Reflect)] + #[reflect(Event)] + struct Pass; + + #[derive(Resource)] + struct TestResult(pub bool); + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let mut world = World::new(); + world.add_observer(move |_event: On, mut result: ResMut| result.0 = true); + world.insert_resource(TestResult(false)); + world.insert_resource(atr); + + let params = serde_json::to_value(&BrpTriggerEventParams { + event: "bevy_remote::builtin_methods::tests::Pass".to_owned(), + value: None, + }) + .expect("FAIL"); + assert_eq!( + process_remote_trigger_event_request(In(Some(params)), &mut world), + Ok(Null) + ); + assert!(world.resource::().0); + } + #[test] fn serialization_tests() { test_serialize_deserialize(BrpQueryRow { diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 4713f88451327..c30a8594900bd 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -455,6 +455,16 @@ //! //! `result`: An array of [fully-qualified type names] of registered resource types. //! +//! ### `world.trigger_event` +//! +//! Triggers an event. +//! +//! `params`: +//! - `event`: The [fully-qualified type name] of the event to trigger. +//! - `value`: The value of the event to trigger. +//! +//! `result`: null. +//! //! ### `registry.schema` //! //! Retrieve schema information about registered types in the Bevy app's type registry. @@ -669,6 +679,10 @@ impl Default for RemotePlugin { builtin_methods::BRP_LIST_RESOURCES_METHOD, builtin_methods::process_remote_list_resources_request, ) + .with_method( + builtin_methods::BRP_TRIGGER_EVENT_METHOD, + builtin_methods::process_remote_trigger_event_request, + ) .with_method( builtin_methods::BRP_REGISTRY_SCHEMA_METHOD, builtin_methods::export_registry_types, @@ -915,7 +929,7 @@ impl From for BrpPayload { } /// An error a request might return. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct BrpError { /// Defines the general type of the error. pub code: i16, diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 10cb2e942133a..c17016db9c4f0 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -1,6 +1,6 @@ //! Module with schemas used for various BRP endpoints use bevy_ecs::{ - reflect::{ReflectComponent, ReflectResource}, + reflect::{ReflectComponent, ReflectEvent, ReflectResource}, resource::Resource, }; use bevy_platform::collections::HashMap; @@ -29,6 +29,7 @@ impl Default for SchemaTypesMetadata { }; data_types.map_type_data::("Component"); data_types.map_type_data::("Resource"); + data_types.map_type_data::("Event"); data_types.map_type_data::("Default"); #[cfg(feature = "bevy_asset")] data_types.map_type_data::("Asset"); From d44b9f8694a418a2aa8a78351c967c3f5c0d51d9 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Mon, 8 Dec 2025 17:31:47 -0500 Subject: [PATCH 048/133] Adds UIDebug Overlay centered scene to examples/testbed/ui (#22052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Fixes #22019 ## Solution - Adds a new scene in the ui testbed that tests the generation of the Ui Debug Overlay. It only is viewable if you run the example with `--features bevy_ui_debug`. - Added a way to override the color used by the overlay to ensure that screenshots are consistent between CI runs (the line colors are generated based on the `Entity`, which changes every time you change the scene.) - Added the `--features bevy_ui_debug` flag to `example-run.yml`, ensuring that screenshots and their validations take this new scene into account. (I assumed I’m not supposed to add this to `validation-jobs.yml` under `run-examples-on-wasm`, but if I’m supposed to, let me know and I can add it. I figured a debug feature does not need that specific validation). ## Testing - I ran the command `cargo run --example testbed_ui --features bevy_ui_debug` and verified that debug lines showed up for regular outlines, for a node with `Visibility::Hidden` with `show_hidden = true`, and that clipped areas have outlines with `show_clipped = true`. I carouseled through all the scenes a couple of times - I also ran `cargo run --example testbed_ui ` and made sure nothing was ruined after multiple carousels through all the scenes. --- ## Showcase `cargo run --example testbed_ui --features bevy_ui_debug` , press spacebar 11x to see the scene. Screenshot 2025-12-06 at 10 03
25 PM --- .github/workflows/example-run.yml | 6 +- crates/bevy_ui_render/src/debug_overlay.rs | 8 +- examples/testbed/ui.rs | 97 ++++++++++++++++++++++ 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/.github/workflows/example-run.yml b/.github/workflows/example-run.yml index 107da778429eb..c41396eab7078 100644 --- a/.github/workflows/example-run.yml +++ b/.github/workflows/example-run.yml @@ -48,7 +48,7 @@ jobs: example_name=`basename $example .ron` echo -n $example_name > last_example_run echo "running $example_name - "`date` - time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome" + time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome,bevy_ui_debug" sleep 10 if [ `find ./ -maxdepth 1 -name 'screenshot-*.png' -print -quit` ]; then mkdir screenshots-$example_name @@ -123,7 +123,7 @@ jobs: example_name=`basename $example .ron` echo -n $example_name > last_example_run echo "running $example_name - "`date` - time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example xvfb-run cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome" + time TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example xvfb-run cargo run --example $example_name --features "bevy_ci_testing,trace,trace_chrome,bevy_ui_debug" sleep 10 if [ `find ./ -maxdepth 1 -name 'screenshot-*.png' -print -quit` ]; then mkdir screenshots-$example_name @@ -187,7 +187,7 @@ jobs: example_name=`basename $example .ron` echo -n $example_name > last_example_run echo "running $example_name - "`date` - time WGPU_BACKEND=dx12 TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example cargo run --example $example_name --features "statically-linked-dxc,bevy_ci_testing,trace,trace_chrome" + time WGPU_BACKEND=dx12 TRACE_CHROME=trace-$example_name.json CI_TESTING_CONFIG=$example cargo run --example $example_name --features "statically-linked-dxc,bevy_ci_testing,trace,trace_chrome,bevy_ui_debug" sleep 10 if [ `find ./ -maxdepth 1 -name 'screenshot-*.png' -print -quit` ]; then mkdir screenshots-$example_name diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index dacc52a195791..c528efecaf82c 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -7,6 +7,7 @@ use crate::shader_flags; use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; use bevy_color::Hsla; +use bevy_color::LinearRgba; use bevy_ecs::entity::Entity; use bevy_ecs::prelude::ReflectResource; use bevy_ecs::resource::Resource; @@ -34,6 +35,8 @@ pub struct UiDebugOptions { pub enabled: bool, /// Width of the overlay's lines in logical pixels pub line_width: f32, + /// Override Color for the overlay's lines + pub line_color_override: Option, /// Show outlines for non-visible UI nodes pub show_hidden: bool, /// Show outlines for clipped sections of UI nodes @@ -51,6 +54,7 @@ impl Default for UiDebugOptions { Self { enabled: false, line_width: 1., + line_color_override: None, show_hidden: false, show_clipped: false, } @@ -101,7 +105,9 @@ pub fn extract_debug_overlay( extracted_camera_entity, transform: transform.into(), item: ExtractedUiItem::Node { - color: Hsla::sequential_dispersed(entity.index_u32()).into(), + color: debug_options + .line_color_override + .unwrap_or_else(|| Hsla::sequential_dispersed(entity.index_u32()).into()), rect: Rect { min: Vec2::ZERO, max: uinode.size, diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 956dead5b1494..652408632e727 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -25,6 +25,12 @@ fn main() { .add_systems(OnEnter(Scene::Transformations), transformations::setup) .add_systems(Update, switch_scene); + #[cfg(feature = "bevy_ui_debug")] + { + app.add_systems(OnEnter(Scene::DebugOutlines), debug_outlines::setup); + app.add_systems(OnExit(Scene::DebugOutlines), debug_outlines::teardown); + } + #[cfg(feature = "bevy_ci_testing")] app.add_systems(Update, helpers::switch_scene_in_ci::); @@ -47,6 +53,8 @@ enum Scene { LinearGradient, RadialGradient, Transformations, + #[cfg(feature = "bevy_ui_debug")] + DebugOutlines, } impl Next for Scene { @@ -62,6 +70,11 @@ impl Next for Scene { Scene::Slice => Scene::LayoutRounding, Scene::LayoutRounding => Scene::LinearGradient, Scene::LinearGradient => Scene::RadialGradient, + #[cfg(feature = "bevy_ui_debug")] + Scene::RadialGradient => Scene::DebugOutlines, + #[cfg(feature = "bevy_ui_debug")] + Scene::DebugOutlines => Scene::Transformations, + #[cfg(not(feature = "bevy_ui_debug"))] Scene::RadialGradient => Scene::Transformations, Scene::Transformations => Scene::Image, } @@ -969,3 +982,87 @@ mod transformations { }); } } + +#[cfg(feature = "bevy_ui_debug")] +mod debug_outlines { + use bevy::{ + color::palettes::css::{BLUE, GRAY, RED}, + prelude::*, + ui_render::UiDebugOptions, + }; + + pub fn setup(mut commands: Commands, mut debug_options: ResMut) { + debug_options.enabled = true; + debug_options.line_width = 5.; + debug_options.line_color_override = Some(LinearRgba::GREEN); + debug_options.show_hidden = true; + debug_options.show_clipped = true; + commands.spawn((Camera2d, DespawnOnExit(super::Scene::DebugOutlines))); + commands + .spawn(( + Node { + width: percent(100), + height: percent(100), + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceAround, + ..default() + }, + DespawnOnExit(super::Scene::DebugOutlines), + )) + .with_children(|parent| { + parent.spawn(( + Node { + width: px(100), + height: px(100), + ..default() + }, + BackgroundColor(GRAY.into()), + UiTransform::from_rotation(Rot2::degrees(45.)), + )); + + parent.spawn((Text::new("Regular Text"), TextFont::default())); + + parent.spawn(( + Node { + width: px(100), + height: px(100), + ..default() + }, + Text::new("Invisible"), + BackgroundColor(GRAY.into()), + TextFont::default(), + Visibility::Hidden, + )); + + parent + .spawn(( + Node { + width: px(100), + height: px(100), + padding: UiRect { + left: px(25), + top: px(25), + ..Default::default() + }, + overflow: Overflow::clip(), + ..default() + }, + BackgroundColor(RED.into()), + )) + .with_children(|child| { + child.spawn(( + Node { + min_width: px(100), + min_height: px(100), + ..default() + }, + BackgroundColor(BLUE.into()), + )); + }); + }); + } + + pub fn teardown(mut debug_options: ResMut) { + *debug_options = UiDebugOptions::default(); + } +} From d76c3aab952239110e4c95767d2c8d21a9a712a6 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 8 Dec 2025 14:43:07 -0800 Subject: [PATCH 049/133] get_components_mut (#21780) # Objective - Add a checked version of `EntityMut::get_components_mut` and `EntityWorldMut::get_components_mut` that does not allocate ## Solution - Add a iterator over the access type to `QueryData`. This is then used to iterate over the pairs of access to check if they are compatible or not. ## Testing - Added a unit test ### Bench checked vs unchecked (50000 entities) | #components | unchecked | checked | times slower | |-------------|-----------|----------|----------| | 2 | 509 us | 1123 us | 2.2x | | 5 | 903 us | 2902us | 3.2x | | 10 | 1700 us | 11424 us | 6.72x | so at 10 components each call was taking about 0.22us vs 0.03 us --- ## ToDo * [x] add release note * [x] add migration guide * [x] add macro for more benches * [x] add bench results to pr description * [ ] look into if this will help with uncached queries * [x] see if we can optimize it a bit --------- Co-authored-by: Alice Cecile --- benches/Cargo.toml | 1 + benches/benches/bevy_ecs/world/mod.rs | 3 + benches/benches/bevy_ecs/world/world_get.rs | 65 ++- crates/bevy_ecs/macros/src/query_data.rs | 12 + crates/bevy_ecs/src/event/trigger.rs | 2 +- crates/bevy_ecs/src/query/access_iter.rs | 467 ++++++++++++++++++ crates/bevy_ecs/src/query/fetch.rs | 100 +++- crates/bevy_ecs/src/query/mod.rs | 10 + .../src/world/entity_access/entity_mut.rs | 96 +++- .../src/world/entity_access/entity_ref.rs | 4 +- .../bevy_ecs/src/world/entity_access/mod.rs | 31 +- .../src/world/entity_access/world_mut.rs | 97 +++- .../bevy_ecs/src/world/unsafe_world_cell.rs | 9 +- crates/bevy_render/src/sync_world.rs | 12 + .../migration-guides/get_components.md | 7 + .../release-notes/get_components_mut.md | 13 + 16 files changed, 906 insertions(+), 23 deletions(-) create mode 100644 crates/bevy_ecs/src/query/access_iter.rs create mode 100644 release-content/migration-guides/get_components.md create mode 100644 release-content/release-notes/get_components_mut.md diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 8940f1eed7105..a8bb418259a46 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -11,6 +11,7 @@ autobenches = false # The primary crate that runs and analyzes our benchmarks. This is a regular dependency because the # `bench!` macro refers to it in its documentation. criterion = { version = "0.8.0", features = ["html_reports"] } +seq-macro = "0.3.6" [dev-dependencies] # Bevy crates diff --git a/benches/benches/bevy_ecs/world/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index 7158f2f033498..b71b59bc69160 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -35,5 +35,8 @@ criterion_group!( query_get_many::<2>, query_get_many::<5>, query_get_many::<10>, + query_get_components_mut_2, + query_get_components_mut_5, + query_get_components_mut_10, entity_set_build_and_lookup, ); diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index dc5c2c5caf420..080990f85d30b 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -1,3 +1,4 @@ +use benches::bench; use core::hint::black_box; use bevy_ecs::{ @@ -5,11 +6,12 @@ use bevy_ecs::{ component::Component, entity::Entity, system::{Query, SystemState}, - world::World, + world::{EntityMut, World}, }; use criterion::Criterion; use rand::{prelude::SliceRandom, SeedableRng}; use rand_chacha::ChaCha8Rng; +use seq_macro::seq; #[derive(Component, Default)] #[component(storage = "Table")] @@ -357,3 +359,64 @@ pub fn query_get_many(criterion: &mut Criterion) { }); } } + +macro_rules! query_get_components_mut { + ($function_name:ident, $val:literal) => { + pub fn $function_name(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group(bench!("world_query_get_components_mut")); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + for entity_count in RANGE.map(|i| i * 10_000) { + seq!(N in 0..$val { + let (mut world, entities) = setup_wide::<( + #(WideTable,)* + )>(entity_count); + }); + let mut query = world.query::(); + group.bench_function(format!("{}_components_{entity_count}_entities", $val), |bencher| { + bencher.iter(|| { + for entity in &entities { + seq!(N in 0..$val { + assert!(query + .get_mut(&mut world, *entity) + .unwrap() + .get_components_mut::<( + #(&mut WideTable,)* + )>() + .is_ok()); + }); + } + }); + }); + group.bench_function( + format!("unchecked_{}_components_{entity_count}_entities", $val), + |bencher| { + bencher.iter(|| { + for entity in &entities { + // SAFETY: no duplicate components are listed + unsafe { + seq!(N in 0..$val { + assert!(query + .get_mut(&mut world, *entity) + .unwrap() + .get_components_mut_unchecked::<( + #(&mut WideTable,)* + )>() + .is_ok()); + }); + } + } + }); + }, + ); + } + + group.finish(); + } + }; +} + +query_get_components_mut!(query_get_components_mut_2, 2); +query_get_components_mut!(query_get_components_mut_5, 5); +query_get_components_mut!(query_get_components_mut_10, 10); diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 1e9dea94bbca3..a91dd193e51c1 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -267,6 +267,12 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { #(#field_members: <#read_only_field_types>::fetch(&_state.#field_aliases, &mut _fetch.#field_aliases, _entity, _table_row)?,)* }) } + + fn iter_access( + _state: &Self::State, + ) -> impl core::iter::Iterator> { + core::iter::empty() #(.chain(<#field_types>::iter_access(&_state.#field_aliases)))* + } } impl #user_impl_generics #path::query::ReleaseStateQueryData @@ -332,6 +338,12 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { #(#field_members: <#field_types>::fetch(&_state.#field_aliases, &mut _fetch.#field_aliases, _entity, _table_row)?,)* }) } + + fn iter_access( + _state: &Self::State, + ) -> impl core::iter::Iterator> { + core::iter::empty() #(.chain(<#field_types>::iter_access(&_state.#field_aliases)))* + } } impl #user_impl_generics #path::query::ReleaseStateQueryData diff --git a/crates/bevy_ecs/src/event/trigger.rs b/crates/bevy_ecs/src/event/trigger.rs index 9492380b63159..ba8a06bc2918d 100644 --- a/crates/bevy_ecs/src/event/trigger.rs +++ b/crates/bevy_ecs/src/event/trigger.rs @@ -301,7 +301,7 @@ unsafe impl< return; } if let Ok(entity) = world.get_entity(current_entity) - && let Some(item) = entity.get_components::() + && let Ok(item) = entity.get_components::() && let Some(traverse_to) = T::traverse(item, event) { current_entity = traverse_to; diff --git a/crates/bevy_ecs/src/query/access_iter.rs b/crates/bevy_ecs/src/query/access_iter.rs new file mode 100644 index 0000000000000..9b79b8156edef --- /dev/null +++ b/crates/bevy_ecs/src/query/access_iter.rs @@ -0,0 +1,467 @@ +use core::fmt::Display; + +use crate::{ + component::{ComponentId, Components}, + query::{Access, QueryData}, +}; + +/// Check if `Q` has any internal conflicts. +#[inline(never)] +pub fn has_conflicts(components: &Components) -> Result<(), QueryAccessError> { + // increasing this too much may slow down smaller queries + const MAX_SIZE: usize = 16; + let Some(state) = Q::get_state(components) else { + return Err(QueryAccessError::ComponentNotRegistered); + }; + let iter = Q::iter_access(&state).enumerate(); + let size = iter.size_hint().1.unwrap_or(MAX_SIZE); + + if size > MAX_SIZE { + for (i, access) in iter { + for access_other in Q::iter_access(&state).take(i) { + if let Err(err) = access.is_compatible(access_other) { + panic!("{}", err); + } + } + } + } else { + // we can optimize small sizes by caching the iteration result in an array on the stack + let mut inner_access = [EcsAccessType::Empty; MAX_SIZE]; + for (i, access) in iter { + for access_other in inner_access.iter().take(i) { + if let Err(err) = access.is_compatible(*access_other) { + panic!("{}", err); + } + } + inner_access[i] = access; + } + } + + Ok(()) +} + +/// The data storage type that is being accessed. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum EcsAccessType<'a> { + /// Accesses [`Component`](crate::prelude::Component) data + Component(EcsAccessLevel), + /// Accesses [`Resource`](crate::prelude::Resource) data + Resource(ResourceAccessLevel), + /// borrowed access from [`WorldQuery::State`](crate::query::WorldQuery) + Access(&'a Access), + /// Does not access any data that can conflict. + Empty, +} + +impl<'a> EcsAccessType<'a> { + /// Returns `Ok(())` if `self` and `other` are compatible. Returns a [`AccessConflictError`] otherwise. + #[inline(never)] + pub fn is_compatible(&self, other: Self) -> Result<(), AccessConflictError<'_>> { + use EcsAccessLevel::*; + use EcsAccessType::*; + + match (*self, other) { + (Component(ReadAll), Component(Write(_))) + | (Component(Write(_)), Component(ReadAll)) + | (Component(_), Component(WriteAll)) + | (Component(WriteAll), Component(_)) => Err(AccessConflictError(*self, other)), + + (Empty, _) + | (_, Empty) + | (Component(_), Resource(_)) + | (Resource(_), Component(_)) + // read only access doesn't conflict + | (Component(Read(_)), Component(Read(_))) + | (Component(ReadAll), Component(Read(_))) + | (Component(Read(_)), Component(ReadAll)) + | (Component(ReadAll), Component(ReadAll)) + | (Resource(ResourceAccessLevel::Read(_)), Resource(ResourceAccessLevel::Read(_))) => { + Ok(()) + } + + (Component(Read(id)), Component(Write(id_other))) + | (Component(Write(id)), Component(Read(id_other))) + | (Component(Write(id)), Component(Write(id_other))) + | ( + Resource(ResourceAccessLevel::Read(id)), + Resource(ResourceAccessLevel::Write(id_other)), + ) + | ( + Resource(ResourceAccessLevel::Write(id)), + Resource(ResourceAccessLevel::Read(id_other)), + ) + | ( + Resource(ResourceAccessLevel::Write(id)), + Resource(ResourceAccessLevel::Write(id_other)), + ) => if id == id_other { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + + // Borrowed Access + (Component(Read(component_id)), Access(access)) + | (Access(access), Component(Read(component_id))) => if access.has_component_write(component_id) { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + + (Component(Write(component_id)), Access(access)) + | (Access(access), Component(Write(component_id))) => if access.has_component_read(component_id) { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + + (Component(ReadAll), Access(access)) + | (Access(access), Component(ReadAll)) => if access.has_any_component_write() { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + + (Component(WriteAll), Access(access)) + | (Access(access), Component(WriteAll))=> if access.has_any_component_read() { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + + (Resource(ResourceAccessLevel::Read(component_id)), Access(access)) + | (Access(access), Resource(ResourceAccessLevel::Read(component_id))) => if access.has_resource_write(component_id) { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + (Resource(ResourceAccessLevel::Write(component_id)), Access(access)) + | (Access(access), Resource(ResourceAccessLevel::Write(component_id))) => if access.has_resource_read(component_id) { + Err(AccessConflictError(*self, other)) + } else { + Ok(()) + }, + + (Access(access), Access(other_access)) => if access.is_compatible(other_access) { + Ok(()) + } else { + Err(AccessConflictError(*self, other)) + }, + } + } +} + +/// The way the data will be accessed and whether we take access on all the components on +/// an entity or just one component. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum EcsAccessLevel { + /// Reads [`Component`](crate::prelude::Component) with [`ComponentId`] + Read(ComponentId), + /// Writes [`Component`](crate::prelude::Component) with [`ComponentId`] + Write(ComponentId), + /// Potentially reads all [`Component`](crate::prelude::Component)'s in the [`World`](crate::prelude::World) + ReadAll, + /// Potentially writes all [`Component`](crate::prelude::Component)'s in the [`World`](crate::prelude::World) + WriteAll, +} + +/// Access level needed by [`QueryData`] fetch to the resource. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ResourceAccessLevel { + /// Reads the resource with [`ComponentId`] + Read(ComponentId), + /// Writes the resource with [`ComponentId`] + Write(ComponentId), +} + +/// Error returned from [`EcsAccessType::is_compatible`] +pub struct AccessConflictError<'a>(EcsAccessType<'a>, EcsAccessType<'a>); + +impl Display for AccessConflictError<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use EcsAccessLevel::*; + use EcsAccessType::*; + + let AccessConflictError(a, b) = self; + match (a, b) { + // ReadAll/WriteAll + Component conflicts + (Component(ReadAll), Component(Write(id))) + | (Component(Write(id)), Component(ReadAll)) => { + write!( + f, + "Component read all access conflicts with component {id:?} write." + ) + } + (Component(WriteAll), Component(Write(id))) + | (Component(Write(id)), Component(WriteAll)) => { + write!( + f, + "Component write all access conflicts with component {id:?} write." + ) + } + (Component(WriteAll), Component(Read(id))) + | (Component(Read(id)), Component(WriteAll)) => { + write!( + f, + "Component write all access conflicts with component {id:?} read." + ) + } + (Component(WriteAll), Component(ReadAll)) + | (Component(ReadAll), Component(WriteAll)) => { + write!(f, "Component write all conflicts with component read all.") + } + (Component(WriteAll), Component(WriteAll)) => { + write!(f, "Component write all conflicts with component write all.") + } + + // Component + Component conflicts + (Component(Read(id)), Component(Write(id_other))) + | (Component(Write(id_other)), Component(Read(id))) => write!( + f, + "Component {id:?} read conflicts with component {id_other:?} write." + ), + (Component(Write(id)), Component(Write(id_other))) => write!( + f, + "Component {id:?} write conflicts with component {id_other:?} write." + ), + + // Borrowed Access conflicts + (Access(_), Component(Read(id))) | (Component(Read(id)), Access(_)) => write!( + f, + "Access has a write that conflicts with component {id:?} read." + ), + (Access(_), Component(Write(id))) | (Component(Write(id)), Access(_)) => write!( + f, + "Access has a read that conflicts with component {id:?} write." + ), + (Access(_), Component(ReadAll)) | (Component(ReadAll), Access(_)) => write!( + f, + "Access has a write that conflicts with component read all" + ), + (Access(_), Component(WriteAll)) | (Component(WriteAll), Access(_)) => write!( + f, + "Access has a read that conflicts with component write all" + ), + (Access(_), Resource(ResourceAccessLevel::Read(id))) + | (Resource(ResourceAccessLevel::Read(id)), Access(_)) => write!( + f, + "Access has a write that conflicts with resource {id:?} read." + ), + (Access(_), Resource(ResourceAccessLevel::Write(id))) + | (Resource(ResourceAccessLevel::Write(id)), Access(_)) => write!( + f, + "Access has a read that conflicts with resource {id:?} write." + ), + (Access(_), Access(_)) => write!(f, "Access conflicts with other Access"), + + _ => { + unreachable!("Other accesses should be compatible"); + } + } + } +} + +/// Error returned from [`has_conflicts`]. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum QueryAccessError { + /// Component was not registered on world + ComponentNotRegistered, + /// Entity did not have the requested components + EntityDoesNotMatch, +} + +impl core::error::Error for QueryAccessError {} + +impl Display for QueryAccessError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match *self { + QueryAccessError::ComponentNotRegistered => { + write!( + f, + "At least one component in Q was not registered in world. + Consider calling `World::register_component`" + ) + } + QueryAccessError::EntityDoesNotMatch => { + write!(f, "Entity does not match Q") + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + prelude::Component, + world::{EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, World}, + }; + + #[derive(Component)] + struct C1; + + #[derive(Component)] + struct C2; + + fn setup_world() -> World { + let world = World::new(); + let mut world = world; + world.register_component::(); + world.register_component::(); + world + } + + #[test] + fn simple_compatible() { + let world = setup_world(); + let c = world.components(); + + // Compatible + assert!(has_conflicts::<&mut C1>(c).is_ok()); + assert!(has_conflicts::<&C1>(c).is_ok()); + assert!(has_conflicts::<(&C1, &C1)>(c).is_ok()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn conflict_component_read_conflicts_write() { + let _ = has_conflicts::<(&C1, &mut C1)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn conflict_component_write_conflicts_read() { + let _ = has_conflicts::<(&mut C1, &C1)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn conflict_component_write_conflicts_write() { + let _ = has_conflicts::<(&mut C1, &mut C1)>(setup_world().components()); + } + + #[test] + fn entity_ref_compatible() { + let world = setup_world(); + let c = world.components(); + + // Compatible + assert!(has_conflicts::<(EntityRef, &C1)>(c).is_ok()); + assert!(has_conflicts::<(&C1, EntityRef)>(c).is_ok()); + assert!(has_conflicts::<(EntityRef, EntityRef)>(c).is_ok()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_ref_conflicts_component_write() { + let _ = has_conflicts::<(EntityRef, &mut C1)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn component_write_conflicts_entity_ref() { + let _ = has_conflicts::<(&mut C1, EntityRef)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_mut_conflicts_component_read() { + let _ = has_conflicts::<(EntityMut, &C1)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn component_read_conflicts_entity_mut() { + let _ = has_conflicts::<(&C1, EntityMut)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_mut_conflicts_component_write() { + let _ = has_conflicts::<(EntityMut, &mut C1)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn component_write_conflicts_entity_mut() { + let _ = has_conflicts::<(&mut C1, EntityMut)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_mut_conflicts_entity_ref() { + let _ = has_conflicts::<(EntityMut, EntityRef)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_ref_conflicts_entity_mut() { + let _ = has_conflicts::<(EntityRef, EntityMut)>(setup_world().components()); + } + + #[test] + fn entity_ref_except_compatible() { + let world = setup_world(); + let c = world.components(); + + // Compatible + assert!(has_conflicts::<(EntityRefExcept, &mut C1)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, EntityRefExcept)>(c).is_ok()); + assert!(has_conflicts::<(&C2, EntityRefExcept)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, EntityRefExcept<(C1, C2)>,)>(c).is_ok()); + assert!(has_conflicts::<(EntityRefExcept<(C1, C2)>, &mut C1,)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, &mut C2, EntityRefExcept<(C1, C2)>,)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, EntityRefExcept<(C1, C2)>, &mut C2,)>(c).is_ok()); + assert!(has_conflicts::<(EntityRefExcept<(C1, C2)>, &mut C1, &mut C2,)>(c).is_ok()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_ref_except_conflicts_component_write() { + let _ = has_conflicts::<(EntityRefExcept, &mut C2)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn component_write_conflicts_entity_ref_except() { + let _ = has_conflicts::<(&mut C2, EntityRefExcept)>(setup_world().components()); + } + + #[test] + fn entity_mut_except_compatible() { + let world = setup_world(); + let c = world.components(); + + // Compatible + assert!(has_conflicts::<(EntityMutExcept, &mut C1)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, EntityMutExcept)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, EntityMutExcept<(C1, C2)>,)>(c).is_ok()); + assert!(has_conflicts::<(EntityMutExcept<(C1, C2)>, &mut C1,)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, &mut C2, EntityMutExcept<(C1, C2)>,)>(c).is_ok()); + assert!(has_conflicts::<(&mut C1, EntityMutExcept<(C1, C2)>, &mut C2,)>(c).is_ok()); + assert!(has_conflicts::<(EntityMutExcept<(C1, C2)>, &mut C1, &mut C2,)>(c).is_ok()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_mut_except_conflicts_component_read() { + let _ = has_conflicts::<(EntityMutExcept, &C2)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn component_read_conflicts_entity_mut_except() { + let _ = has_conflicts::<(&C2, EntityMutExcept)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn entity_mut_except_conflicts_component_write() { + let _ = has_conflicts::<(EntityMutExcept, &mut C2)>(setup_world().components()); + } + + #[test] + #[should_panic(expected = "conflicts")] + fn component_write_conflicts_entity_mut_except() { + let _ = has_conflicts::<(&mut C2, EntityMutExcept)>(setup_world().components()); + } +} diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 2cab429557c7c..d0b191513e8ed 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -4,7 +4,10 @@ use crate::{ change_detection::{ComponentTicksMut, ComponentTicksRef, MaybeLocation, Tick}, component::{Component, ComponentId, Components, Mutable, StorageType}, entity::{Entities, Entity, EntityLocation}, - query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery}, + query::{ + access_iter::{EcsAccessLevel, EcsAccessType}, + Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery, + }, storage::{ComponentSparseSet, Table, TableRow}, world::{ unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, @@ -13,7 +16,7 @@ use crate::{ }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; use bevy_utils::prelude::DebugName; -use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; +use core::{cell::UnsafeCell, iter, marker::PhantomData, panic::Location}; use variadics_please::all_tuples; /// Types that can be fetched from a [`World`] using a [`Query`]. @@ -342,6 +345,11 @@ pub unsafe trait QueryData: WorldQuery { entity: Entity, table_row: TableRow, ) -> Option>; + + /// Returns an iterator over the access needed by [`QueryData::fetch`]. Access conflicts are usually + /// checked in [`WorldQuery::update_component_access`], but in certain cases this method can be useful to implement + /// a way of checking for access conflicts in a non-allocating way. + fn iter_access(state: &Self::State) -> impl Iterator>; } /// A [`QueryData`] that is read only. @@ -450,6 +458,10 @@ unsafe impl QueryData for Entity { ) -> Option> { Some(entity) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: access is read only @@ -543,6 +555,10 @@ unsafe impl QueryData for EntityLocation { // SAFETY: `fetch` must be called with an entity that exists in the world Some(unsafe { fetch.get_spawned(entity).debug_checked_unwrap() }) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: access is read only @@ -719,6 +735,10 @@ unsafe impl QueryData for SpawnDetails { this_run: fetch.this_run, }) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: access is read only @@ -838,6 +858,10 @@ unsafe impl<'a> QueryData for EntityRef<'a> { // SAFETY: Read-only access to every component has been registered. Some(unsafe { EntityRef::new(cell) }) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Component(EcsAccessLevel::ReadAll)) + } } /// SAFETY: access is read only @@ -944,6 +968,10 @@ unsafe impl<'a> QueryData for EntityMut<'a> { // SAFETY: mutable access to every component has been registered. Some(unsafe { EntityMut::new(cell) }) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Component(EcsAccessLevel::WriteAll)) + } } impl ReleaseStateQueryData for EntityMut<'_> { @@ -1020,7 +1048,7 @@ unsafe impl WorldQuery for FilteredEntityRef<'_, '_> { } /// SAFETY: `Self` is the same as `Self::ReadOnly` -unsafe impl QueryData for FilteredEntityRef<'_, '_> { +unsafe impl<'a, 'b> QueryData for FilteredEntityRef<'a, 'b> { const IS_READ_ONLY: bool = true; const IS_ARCHETYPAL: bool = true; type ReadOnly = Self; @@ -1068,6 +1096,10 @@ unsafe impl QueryData for FilteredEntityRef<'_, '_> { // SAFETY: mutable access to every component has been registered. Some(unsafe { FilteredEntityRef::new(cell, access) }) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Access(state)) + } } /// SAFETY: Access is read-only. @@ -1187,6 +1219,10 @@ unsafe impl<'a, 'b> QueryData for FilteredEntityMut<'a, 'b> { // SAFETY: mutable access to every component has been registered. Some(unsafe { FilteredEntityMut::new(cell, access) }) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Access(state)) + } } impl ArchetypeQueryData for FilteredEntityMut<'_, '_> {} @@ -1297,6 +1333,10 @@ where .unwrap(); Some(EntityRefExcept::new(cell, access)) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Access(state)) + } } /// SAFETY: `EntityRefExcept` enforces read-only access to its contained @@ -1412,6 +1452,10 @@ where .unwrap(); Some(EntityMutExcept::new(cell, access)) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Access(state)) + } } impl ArchetypeQueryData for EntityMutExcept<'_, '_, B> {} @@ -1499,6 +1543,10 @@ unsafe impl QueryData for &Archetype { // SAFETY: The assigned archetype for a living entity must always be valid. Some(unsafe { archetypes.get(location.archetype_id).debug_checked_unwrap() }) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: access is read only @@ -1668,6 +1716,10 @@ unsafe impl QueryData for &T { }, )) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Component(EcsAccessLevel::Read(*state))) + } } /// SAFETY: access is read only @@ -1881,6 +1933,10 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { }, )) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Component(EcsAccessLevel::Read(*state))) + } } /// SAFETY: access is read only @@ -2094,6 +2150,10 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T }, )) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Component(EcsAccessLevel::Write(*state))) + } } impl> ReleaseStateQueryData for &mut T { @@ -2209,6 +2269,10 @@ unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> ) -> Option> { <&mut T as QueryData>::fetch(state, fetch, entity, table_row) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + iter::once(EcsAccessType::Component(EcsAccessLevel::Write(*state))) + } } impl> ReleaseStateQueryData for Mut<'_, T> { @@ -2357,6 +2421,10 @@ unsafe impl QueryData for Option { .flatten(), ) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + T::iter_access(state) + } } /// SAFETY: [`OptionFetch`] is read only because `T` is read only @@ -2533,6 +2601,10 @@ unsafe impl QueryData for Has { ) -> Option> { Some(*fetch) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: [`Has`] is read only @@ -2608,6 +2680,11 @@ macro_rules! impl_tuple_query_data { // SAFETY: The invariants are upheld by the caller. Some(($(unsafe { $name::fetch($state, $name, entity, table_row) }?,)*)) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + let ($($name,)*) = state; + iter::empty()$(.chain($name::iter_access($name)))* + } } $(#[$meta])* @@ -2805,6 +2882,11 @@ macro_rules! impl_anytuple_fetch { || !(false $(|| $name.1)*)) .then_some(result) } + + fn iter_access(state: &Self::State) -> impl Iterator> { + let ($($name,)*) = state; + iter::empty()$(.chain($name::iter_access($name)))* + } } $(#[$meta])* @@ -2926,6 +3008,10 @@ unsafe impl QueryData for NopWorldQuery { ) -> Option> { Some(()) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: `NopFetch` never accesses any data @@ -3011,6 +3097,10 @@ unsafe impl QueryData for PhantomData { ) -> Option> { Some(()) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: `PhantomData` never accesses any world data. @@ -3219,6 +3309,10 @@ mod tests { ) -> Option> { Some(()) } + + fn iter_access(_state: &Self::State) -> impl Iterator> { + iter::empty() + } } /// SAFETY: access is read only diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index eb3dad12bd367..b5c1b3d7d18a5 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -1,6 +1,7 @@ //! Contains APIs for retrieving component data from the world. mod access; +mod access_iter; mod builder; mod error; mod fetch; @@ -11,6 +12,7 @@ mod state; mod world_query; pub use access::*; +pub use access_iter::*; pub use bevy_ecs_macros::{QueryData, QueryFilter}; pub use builder::*; pub use error::*; @@ -897,6 +899,14 @@ mod tests { ) -> Option> { Some(()) } + + fn iter_access( + state: &Self::State, + ) -> impl Iterator> { + core::iter::once(super::access_iter::EcsAccessType::Resource( + super::access_iter::ResourceAccessLevel::Read(*state), + )) + } } /// SAFETY: access is read only diff --git a/crates/bevy_ecs/src/world/entity_access/entity_mut.rs b/crates/bevy_ecs/src/world/entity_access/entity_mut.rs index c7ecdb4adb62f..ed6ffbe1c9109 100644 --- a/crates/bevy_ecs/src/world/entity_access/entity_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/entity_mut.rs @@ -3,7 +3,7 @@ use crate::{ change_detection::{ComponentTicks, MaybeLocation, Tick}, component::{Component, ComponentId, Mutable}, entity::{ContainsEntity, Entity, EntityEquivalent, EntityLocation}, - query::{Access, ReadOnlyQueryData, ReleaseStateQueryData}, + query::{has_conflicts, Access, QueryAccessError, ReadOnlyQueryData, ReleaseStateQueryData}, world::{ error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, DynamicComponentFetch, EntityRef, FilteredEntityMut, FilteredEntityRef, Mut, Ref, @@ -152,7 +152,7 @@ impl<'w> EntityMut<'w> { /// or `None` if the entity does not have the components required by the query `Q`. pub fn get_components( &self, - ) -> Option> { + ) -> Result, QueryAccessError> { self.as_readonly().get_components::() } @@ -184,13 +184,46 @@ impl<'w> EntityMut<'w> { /// # Safety /// It is the caller's responsibility to ensure that /// the `QueryData` does not provide aliasing mutable references to the same component. + /// + /// # See also + /// + /// - [`Self::get_components_mut`] for the safe version that performs aliasing checks pub unsafe fn get_components_mut_unchecked( &mut self, - ) -> Option> { - // SAFETY: Caller the `QueryData` does not provide aliasing mutable references to the same component + ) -> Result, QueryAccessError> { + // SAFETY: Caller ensures the `QueryData` does not provide aliasing mutable references to the same component unsafe { self.reborrow().into_components_mut_unchecked::() } } + /// Returns components for the current entity that match the query `Q`. + /// In the case of conflicting [`QueryData`](crate::query::QueryData), unregistered components, or missing components, + /// this will return a [`QueryAccessError`] + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Component)] + /// struct X(usize); + /// #[derive(Component)] + /// struct Y(usize); + /// + /// # let mut world = World::default(); + /// let mut entity = world.spawn((X(0), Y(0))).into_mutable(); + /// // Get mutable access to two components at once + /// // SAFETY: X and Y are different components + /// let (mut x, mut y) = entity.get_components_mut::<(&mut X, &mut Y)>().unwrap(); + /// ``` + /// + /// Note that this does a O(n^2) check that the [`QueryData`](crate::query::QueryData) does not conflict. If performance is a + /// consideration you should use [`Self::get_components_mut_unchecked`] instead. + pub fn get_components_mut( + &mut self, + ) -> Result, QueryAccessError> { + self.reborrow().into_components_mut::() + } + /// Consumes self and returns components for the current entity that match the query `Q` for the world lifetime `'w`, /// or `None` if the entity does not have the components required by the query `Q`. /// @@ -219,15 +252,68 @@ impl<'w> EntityMut<'w> { /// # Safety /// It is the caller's responsibility to ensure that /// the `QueryData` does not provide aliasing mutable references to the same component. + /// + /// # See also + /// + /// - [`Self::into_components_mut`] for the safe version that performs aliasing checks pub unsafe fn into_components_mut_unchecked( self, - ) -> Option> { + ) -> Result, QueryAccessError> { // SAFETY: // - We have mutable access to all components of this entity. // - Caller asserts the `QueryData` does not provide aliasing mutable references to the same component unsafe { self.cell.get_components::() } } + /// Consumes self and returns components for the current entity that match the query `Q` for the world lifetime `'w`, + /// or `None` if the entity does not have the components required by the query `Q`. + /// + /// The checks for aliasing mutable references may be expensive. + /// If performance is a concern, consider making multiple calls to [`Self::get_mut`]. + /// If that is not possible, consider using [`Self::into_components_mut_unchecked`] to skip the checks. + /// + /// # Panics + /// + /// If the `QueryData` provides aliasing mutable references to the same component. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Component)] + /// struct X(usize); + /// #[derive(Component)] + /// struct Y(usize); + /// + /// # let mut world = World::default(); + /// let mut entity = world.spawn((X(0), Y(0))).into_mutable(); + /// // Get mutable access to two components at once + /// let (mut x, mut y) = entity.into_components_mut::<(&mut X, &mut Y)>().unwrap(); + /// *x = X(1); + /// *y = Y(1); + /// ``` + /// + /// ```should_panic + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct X(usize); + /// # + /// # let mut world = World::default(); + /// let mut entity = world.spawn((X(0))).into_mutable(); + /// // This panics, as the `&mut X`s would alias: + /// entity.into_components_mut::<(&mut X, &mut X)>(); + /// ``` + pub fn into_components_mut( + self, + ) -> Result, QueryAccessError> { + has_conflicts::(self.cell.world().components())?; + + // SAFETY: we checked that there were not conflicting components above + unsafe { self.into_components_mut_unchecked::() } + } + /// Consumes `self` and gets access to the component of type `T` with the /// world `'w` lifetime for the current entity. /// diff --git a/crates/bevy_ecs/src/world/entity_access/entity_ref.rs b/crates/bevy_ecs/src/world/entity_access/entity_ref.rs index 9978c775bc4b7..abe884a1ccf0e 100644 --- a/crates/bevy_ecs/src/world/entity_access/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_access/entity_ref.rs @@ -3,7 +3,7 @@ use crate::{ change_detection::{ComponentTicks, MaybeLocation, Tick}, component::{Component, ComponentId}, entity::{ContainsEntity, Entity, EntityEquivalent, EntityLocation}, - query::{Access, ReadOnlyQueryData, ReleaseStateQueryData}, + query::{Access, QueryAccessError, ReadOnlyQueryData, ReleaseStateQueryData}, world::{ error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, DynamicComponentFetch, FilteredEntityRef, Ref, @@ -270,7 +270,7 @@ impl<'w> EntityRef<'w> { /// or `None` if the entity does not have the components required by the query `Q`. pub fn get_components( &self, - ) -> Option> { + ) -> Result, QueryAccessError> { // SAFETY: // - We have read-only access to all components of this entity. // - The query is read-only, and read-only references cannot have conflicts. diff --git a/crates/bevy_ecs/src/world/entity_access/mod.rs b/crates/bevy_ecs/src/world/entity_access/mod.rs index 187540f868995..cc1bd85449761 100644 --- a/crates/bevy_ecs/src/world/entity_access/mod.rs +++ b/crates/bevy_ecs/src/world/entity_access/mod.rs @@ -23,6 +23,7 @@ mod tests { use crate::change_detection::Tick; use crate::lifecycle::HookContext; + use crate::query::QueryAccessError; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -815,11 +816,35 @@ mod tests { let e3 = world.spawn_empty().id(); assert_eq!( - Some((&X(7), &Y(10))), + Ok((&X(7), &Y(10))), + world.entity(e1).get_components::<(&X, &Y)>() + ); + assert_eq!( + Err(QueryAccessError::EntityDoesNotMatch), + world.entity(e2).get_components::<(&X, &Y)>() + ); + assert_eq!( + Err(QueryAccessError::EntityDoesNotMatch), + world.entity(e3).get_components::<(&X, &Y)>() + ); + } + + #[test] + fn get_components_mut() { + let mut world = World::default(); + let e1 = world.spawn((X(7), Y(10))).id(); + + let mut entity_mut_1 = world.entity_mut(e1); + let Ok((mut x, mut y)) = entity_mut_1.get_components_mut::<(&mut X, &mut Y)>() else { + panic!("could not get components"); + }; + x.0 += 1; + y.0 += 1; + + assert_eq!( + Ok((&X(8), &Y(11))), world.entity(e1).get_components::<(&X, &Y)>() ); - assert_eq!(None, world.entity(e2).get_components::<(&X, &Y)>()); - assert_eq!(None, world.entity(e3).get_components::<(&X, &Y)>()); } #[test] diff --git a/crates/bevy_ecs/src/world/entity_access/world_mut.rs b/crates/bevy_ecs/src/world/entity_access/world_mut.rs index 1ca4e24c75f5b..97cb0e4007584 100644 --- a/crates/bevy_ecs/src/world/entity_access/world_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/world_mut.rs @@ -9,7 +9,10 @@ use crate::{ event::{EntityComponentsTrigger, EntityEvent}, lifecycle::{Despawn, Remove, Replace, DESPAWN, REMOVE, REPLACE}, observer::Observer, - query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, + query::{ + has_conflicts, Access, DebugCheckedUnwrap, QueryAccessError, ReadOnlyQueryData, + ReleaseStateQueryData, + }, relationship::RelationshipHookMode, resource::Resource, storage::{SparseSets, Table}, @@ -284,7 +287,7 @@ impl<'w> EntityWorldMut<'w> { #[inline] pub fn get_components( &self, - ) -> Option> { + ) -> Result, QueryAccessError> { self.as_readonly().get_components::() } @@ -316,13 +319,46 @@ impl<'w> EntityWorldMut<'w> { /// # Safety /// It is the caller's responsibility to ensure that /// the `QueryData` does not provide aliasing mutable references to the same component. + /// + /// /// # See also + /// + /// - [`Self::get_components_mut`] for the safe version that performs aliasing checks pub unsafe fn get_components_mut_unchecked( &mut self, - ) -> Option> { + ) -> Result, QueryAccessError> { // SAFETY: Caller the `QueryData` does not provide aliasing mutable references to the same component unsafe { self.as_mutable().into_components_mut_unchecked::() } } + /// Returns components for the current entity that match the query `Q`. + /// In the case of conflicting [`QueryData`](crate::query::QueryData), unregistered components, or missing components, + /// this will return a [`QueryAccessError`] + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Component)] + /// struct X(usize); + /// #[derive(Component)] + /// struct Y(usize); + /// + /// # let mut world = World::default(); + /// let mut entity = world.spawn((X(0), Y(0))).into_mutable(); + /// // Get mutable access to two components at once + /// // SAFETY: X and Y are different components + /// let (mut x, mut y) = entity.get_components_mut::<(&mut X, &mut Y)>().unwrap(); + /// ``` + /// + /// Note that this does a O(n^2) check that the [`QueryData`](crate::query::QueryData) does not conflict. If performance is a + /// consideration you should use [`Self::get_components_mut_unchecked`] instead. + pub fn get_components_mut( + &mut self, + ) -> Result, QueryAccessError> { + self.as_mutable().into_components_mut::() + } + /// Consumes self and returns components for the current entity that match the query `Q` for the world lifetime `'w`, /// or `None` if the entity does not have the components required by the query `Q`. /// @@ -351,13 +387,66 @@ impl<'w> EntityWorldMut<'w> { /// # Safety /// It is the caller's responsibility to ensure that /// the `QueryData` does not provide aliasing mutable references to the same component. + /// + /// # See also + /// + /// - [`Self::into_components_mut`] for the safe version that performs aliasing checks pub unsafe fn into_components_mut_unchecked( self, - ) -> Option> { + ) -> Result, QueryAccessError> { // SAFETY: Caller the `QueryData` does not provide aliasing mutable references to the same component unsafe { self.into_mutable().into_components_mut_unchecked::() } } + /// Consumes self and returns components for the current entity that match the query `Q` for the world lifetime `'w`, + /// or `None` if the entity does not have the components required by the query `Q`. + /// + /// The checks for aliasing mutable references may be expensive. + /// If performance is a concern, consider making multiple calls to [`Self::get_mut`]. + /// If that is not possible, consider using [`Self::into_components_mut_unchecked`] to skip the checks. + /// + /// # Panics + /// + /// - If the `QueryData` provides aliasing mutable references to the same component. + /// - If the entity has been despawned while this `EntityWorldMut` is still alive. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Component)] + /// struct X(usize); + /// #[derive(Component)] + /// struct Y(usize); + /// + /// # let mut world = World::default(); + /// let mut entity = world.spawn((X(0), Y(0))); + /// // Get mutable access to two components at once + /// let (mut x, mut y) = entity.into_components_mut::<(&mut X, &mut Y)>().unwrap(); + /// *x = X(1); + /// *y = Y(1); + /// ``` + /// + /// ```should_panic + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component)] + /// # struct X(usize); + /// # + /// # let mut world = World::default(); + /// let mut entity = world.spawn((X(0))); + /// // This panics, as the `&mut X`s would alias: + /// entity.into_components_mut::<(&mut X, &mut X)>(); + /// ``` + pub fn into_components_mut( + self, + ) -> Result, QueryAccessError> { + has_conflicts::(self.world.components())?; + // SAFETY: we checked that there were not conflicting components above + unsafe { self.into_mutable().into_components_mut_unchecked::() } + } + /// Consumes `self` and gets access to the component of type `T` with /// the world `'w` lifetime for the current entity. /// Returns `None` if the entity does not have a component of type `T`. diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 3b450e066e241..54a835e2863ab 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -16,7 +16,7 @@ use crate::{ lifecycle::RemovedComponentMessages, observer::Observers, prelude::Component, - query::{DebugCheckedUnwrap, ReleaseStateQueryData}, + query::{DebugCheckedUnwrap, QueryAccessError, ReleaseStateQueryData}, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -983,11 +983,11 @@ impl<'w> UnsafeEntityCell<'w> { /// - The `QueryData` does not provide aliasing mutable references to the same component. pub(crate) unsafe fn get_components( &self, - ) -> Option> { + ) -> Result, QueryAccessError> { // SAFETY: World is only used to access query data and initialize query state let state = unsafe { let world = self.world().world(); - Q::get_state(world.components())? + Q::get_state(world.components()).ok_or(QueryAccessError::ComponentNotRegistered)? }; let location = self.location(); // SAFETY: Location is guaranteed to exist @@ -1015,8 +1015,9 @@ impl<'w> UnsafeEntityCell<'w> { // SAFETY: Called after set_archetype above. Entity and location are guaranteed to exist. let item = unsafe { Q::fetch(&state, &mut fetch, self.id(), location.table_row) }; item.map(Q::release_state) + .ok_or(QueryAccessError::EntityDoesNotMatch) } else { - None + Err(QueryAccessError::EntityDoesNotMatch) } } diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index bac8c4a3c6202..6a1a1fa813a21 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -385,6 +385,12 @@ mod render_entities_world_query_impls { unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.map(RenderEntity::id) } + + fn iter_access( + state: &Self::State, + ) -> impl Iterator> { + <&RenderEntity as QueryData>::iter_access(state) + } } // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. @@ -494,6 +500,12 @@ mod render_entities_world_query_impls { unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.map(MainEntity::id) } + + fn iter_access( + state: &Self::State, + ) -> impl Iterator> { + <&MainEntity as QueryData>::iter_access(state) + } } // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. diff --git a/release-content/migration-guides/get_components.md b/release-content/migration-guides/get_components.md new file mode 100644 index 0000000000000..f91a5e44300d6 --- /dev/null +++ b/release-content/migration-guides/get_components.md @@ -0,0 +1,7 @@ +--- +title: get_components, get_components_mut_unchecked now return a Result +pull_requests: [21780] +--- + +`get_components`, `get_components_mut_unchecked`, and `into_components_mut_unchecked` +now return a `Result<_, QueryAccessError>` instead of an `Option`. diff --git a/release-content/release-notes/get_components_mut.md b/release-content/release-notes/get_components_mut.md new file mode 100644 index 0000000000000..85a300e9f1496 --- /dev/null +++ b/release-content/release-notes/get_components_mut.md @@ -0,0 +1,13 @@ +--- +title: get_components_mut +authors: ["@hymm"] +pull_requests: [21780] +--- + +A safe version of `EntityMut::get_components_mut` and `EntityWorldMut::get_components_mut` +was added. Previously a unsafe version was added `get_components_mut_unchecked`. It needed +to be unsafe because specifying (&mut T, &mut T) is possible which would return multiple +mutable references to the same component. This was done by adding a O(n^2) check for +conflicts which returns a `QueryAccessError::Conflict`. Because of the cost of the checks +if your code is performance sensitive it may make sense to keep using +`get_components_mut_unchecked`. From 41bf46d8c3513b1e6a572b7b18bae3f344cefc45 Mon Sep 17 00:00:00 2001 From: Eric Kwoka <43540491+ekwoka@users.noreply.github.com> Date: Tue, 9 Dec 2025 02:44:19 +0400 Subject: [PATCH 050/133] :bug: Fixes PropagateStop and PropagateOver (#21622) # Objective Fixes #21620 Ensures `PropagateStop` works as documented. At it stands, `PropagateStop` is described as only preventing it's Children from inheriting `C` but in practice that entity itself would not inherit `C`. Makes `PropagateOver` work. It just plain didn't work at all. ## Solution Adjusted the flow to prevent recursively inheriting `Inherited` when `PropagateStop` present, as opposed to not accessing `PropagateStop` entities entirely. Skips applying `C` when `Inherited` and `PropagateOver` are on the entity. ## Testing I updated the tests first to verify the test cases. They were incomplete and didn't pass once checking for the proper end state. After verifying those were not functioning, trimmed and adjusted code until it functioned. ## Fuller Explanation To my understanding of the goals (and my implementation) the idea is that the result of these components would be like ``` (Propagate, Inherited, C) (Inherited, C) (PropagateOver, Inherited) (Inherited, C) (PropagateStop, Inherited, C) ``` While the previous (incorrect by my measure) appeared to be ``` (Propagate, Inherited, C) (Inherited, C) (PropagateOver, Inherited, C) (Inherited, C) (PropagateStop) ``` So of note: The `PropagateOver` is still getting `Inherited` in this current adjustment. I believe it was doing that before, but I didn't fully understand the reasoning for the `Inherited` at all? I guess it's to properly mark that the `C` is an inherited one, so maybe I do need to remove it on `PropagateOver` as well, to ensure cleanup doesn't also remove any separately applied `C`? I didn't look closely at what all the other tests are attempting to test (or if they are correctly testing those) to evaluate if other changes need to be made. if `PropagateOver` doesn't have `Inherited` and is reparented, would it be simple to ensure that its children have their `Inherited` removed/updated properly? Looking at the code more, chances are this can be simplified a lot with the use of AncestorIter and DescendentIter, but I am not sure if jumping into a fuller refactor makes sense at this moment as opposed to attempting to fix this immediate issue and following up with a bigger refactor? thoughts? --------- Co-authored-by: robtfm <50659922+robtfm@users.noreply.github.com> --- crates/bevy_app/src/propagate.rs | 506 +++++++++++++++++++++++-------- 1 file changed, 383 insertions(+), 123 deletions(-) diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs index e0a02c86f2df5..ecdfa74b9dfda 100644 --- a/crates/bevy_app/src/propagate.rs +++ b/crates/bevy_app/src/propagate.rs @@ -5,6 +5,7 @@ use crate::{App, Plugin}; #[cfg(feature = "bevy_reflect")] use bevy_ecs::reflect::ReflectComponent; use bevy_ecs::{ + change_detection::DetectChangesMut, component::Component, entity::Entity, hierarchy::ChildOf, @@ -88,7 +89,7 @@ pub struct PropagateSet { } /// Internal struct for managing propagation -#[derive(Component, Clone, PartialEq)] +#[derive(Component, Clone, PartialEq, Debug)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -139,9 +140,9 @@ impl, - update_stopped::, + update_source::, update_reparented::, + update_removed_limit::, propagate_inherited::, propagate_output::, ) @@ -151,17 +152,13 @@ impl` and `C` for entities with a direct `Propagate::` -pub fn update_source( +/// add/remove `Inherited::` for entities with a direct `Propagate::` +pub fn update_source( mut commands: Commands, - changed: Query< - (Entity, &Propagate), - ( - Or<(Changed>, Without>)>, - Without>, - ), - >, + changed: Query<(Entity, &Propagate), (Or<(Changed>, Without>)>,)>, mut removed: RemovedComponents>, + relationship: Query<&R>, + relations: Query<&Inherited, Without>>, ) { for (entity, source) in &changed { commands @@ -169,49 +166,57 @@ pub fn update_source( .try_insert(Inherited(source.0.clone())); } + // set `Inherited::` based on ancestry when `Propagate::` is removed for removed in removed.read() { if let Ok(mut commands) = commands.get_entity(removed) { - commands.remove::<(Inherited, C)>(); + if let Some(inherited) = relationship + .get(removed) + .ok() + .and_then(|r| relations.get(r.get()).ok()) + { + commands.insert(inherited.clone()); + } else { + commands.remove::>(); + } } } } -/// remove `Inherited::` and `C` for entities with a `PropagateStop::` -pub fn update_stopped( - mut commands: Commands, - q: Query>, With>, F)>, -) { - for entity in q.iter() { - let mut cmds = commands.entity(entity); - cmds.remove::<(Inherited, C)>(); - } -} - -/// add/remove `Inherited::` and `C` for entities which have changed relationship +/// add/remove `Inherited::` for entities which have changed relationship pub fn update_reparented( mut commands: Commands, - moved: Query< - (Entity, &R, Option<&Inherited>), - ( - Changed, - Without>, - Without>, - F, - ), - >, - relations: Query<&Inherited>, + moved: Query<(Entity, &R, Option<&Inherited>), (Changed, Without>, F)>, + relations: Query<&Inherited, Without>>, orphaned: Query>, Without>, Without, F)>, ) { for (entity, relation, maybe_inherited) in &moved { if let Ok(inherited) = relations.get(relation.get()) { commands.entity(entity).try_insert(inherited.clone()); } else if maybe_inherited.is_some() { - commands.entity(entity).remove::<(Inherited, C)>(); + commands.entity(entity).remove::>(); } } for orphan in &orphaned { - commands.entity(orphan).remove::<(Inherited, C)>(); + commands.entity(orphan).remove::>(); + } +} + +/// When `PropagateOver` or `PropagateStop` is removed, update the `Inherited::` to trigger propagation +pub fn update_removed_limit( + mut inherited: Query<&mut Inherited>, + mut removed_skip: RemovedComponents>, + mut removed_stop: RemovedComponents>, +) { + for entity in removed_skip.read() { + if let Ok(mut inherited) = inherited.get_mut(entity) { + inherited.set_changed(); + } + } + for entity in removed_stop.read() { + if let Ok(mut inherited) = inherited.get_mut(entity) { + inherited.set_changed(); + } } } @@ -223,8 +228,12 @@ pub fn propagate_inherited>, Without>, F), >, recurse: Query< - (Option<&R::RelationshipTarget>, Option<&Inherited>), - (Without>, Without>, F), + ( + Option<&R::RelationshipTarget>, + Option<&Inherited>, + Option<&PropagateStop>, + ), + (Without>, F), >, mut removed: RemovedComponents>, mut to_process: Local>)>>, @@ -240,14 +249,14 @@ pub fn propagate_inherited` if let Some(inherited) = maybe_inherited { - commands.entity(entity).try_insert(inherited.clone()); + commands.entity(entity).try_insert(inherited); } else { - commands.entity(entity).remove::<(Inherited, C)>(); + commands.entity(entity).remove::>(); } } } -/// add `C` to entities with `Inherited::` +/// add/remove `C` on entities with `Inherited::` pub fn propagate_output( mut commands: Commands, changed: Query< (Entity, &Inherited, Option<&C>), (Changed>, Without>, F), >, + mut removed: RemovedComponents>, + skip: Query<(), With>>, ) { for (entity, inherited, maybe_current) in &changed { if maybe_current.is_some_and(|c| &inherited.0 == c) { @@ -286,6 +301,12 @@ pub fn propagate_output( commands.entity(entity).try_insert(inherited.0.clone()); } + + for removed in removed.read() { + if skip.get(removed).is_err() { + commands.entity(removed).remove::(); + } + } } #[cfg(test)] @@ -305,6 +326,8 @@ mod tests { app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let intermediate = app .world_mut() @@ -319,19 +342,49 @@ mod tests { app.update(); - assert!(app + assert_eq!( + query.get_many(app.world(), [propagator, intermediate, propagatee]), + Ok([&TestValue(1), &TestValue(1), &TestValue(1)]) + ); + } + + #[test] + fn test_remove_propagate() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + + let mut query = app.world_mut().query::<&TestValue>(); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_ok()); + .spawn_empty() + .insert(ChildOf(propagator)) + .id(); + + app.update(); + + assert_eq!(query.get(app.world(), propagatee), Ok(&TestValue(1))); + + app.world_mut() + .commands() + .entity(propagator) + .remove::>(); + app.update(); + + assert!(query.get(app.world(), propagator).is_err()); + assert!(query.get(app.world(), propagatee).is_err()); } #[test] - fn test_reparented() { + fn test_remove_orphan() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagatee = app .world_mut() @@ -341,11 +394,45 @@ mod tests { app.update(); - assert!(app + assert_eq!(query.get(app.world(), propagatee), Ok(&TestValue(1))); + + app.world_mut() + .commands() + .entity(propagatee) + .remove::(); + app.update(); + + assert!(query.get(app.world(), propagatee).is_err()); + } + + #[test] + fn test_reparented() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + + let mut query = app.world_mut().query::<&TestValue>(); + + let propagator_one = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let other_parent = app.world_mut().spawn_empty().id(); + let propagatee = app .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_ok()); + .spawn_empty() + .insert(ChildOf(propagator_one)) + .id(); + + app.update(); + + assert_eq!(query.get(app.world(), propagatee), Ok(&TestValue(1))); + + app.world_mut() + .commands() + .entity(propagatee) + .insert(ChildOf(other_parent)); + + app.update(); + + assert!(query.get(app.world(), propagatee).is_err()); } #[test] @@ -354,6 +441,8 @@ mod tests { app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id(); let propagatee = app @@ -363,98 +452,139 @@ mod tests { .id(); app.update(); - assert_eq!( - app.world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee), - Ok(&TestValue(1)) - ); + + assert_eq!(query.get(app.world(), propagatee), Ok(&TestValue(1))); + app.world_mut() .commands() .entity(propagatee) .insert(ChildOf(propagator_b)); app.update(); - assert_eq!( - app.world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee), - Ok(&TestValue(2)) - ); + + assert_eq!(query.get(app.world(), propagatee), Ok(&TestValue(2))); } #[test] - fn test_remove_orphan() { + fn test_propagate_over() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_over = app + .world_mut() + .spawn(TestValue(2)) + .insert((PropagateOver::::default(), ChildOf(propagator))) + .id(); let propagatee = app .world_mut() .spawn_empty() - .insert(ChildOf(propagator)) + .insert(ChildOf(propagate_over)) .id(); app.update(); - assert!(app + + assert_eq!(query.get(app.world(), propagate_over), Ok(&TestValue(2))); + assert_eq!(query.get(app.world(), propagatee), Ok(&TestValue(1))); + } + + #[test] + fn test_remove_propagate_over() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + + let mut query = app.world_mut().query::<&TestValue>(); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_over = app + .world_mut() + .spawn(TestValue(2)) + .insert((PropagateOver::::default(), ChildOf(propagator))) + .id(); + let propagatee = app .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_ok()); + .spawn_empty() + .insert(ChildOf(propagate_over)) + .id(); + + app.update(); + assert_eq!( + app.world_mut() + .query::<&Inherited>() + .get(app.world(), propagate_over), + Ok(&Inherited(TestValue(1))) + ); + assert_eq!( + app.world_mut() + .query::<&Inherited>() + .get(app.world(), propagatee), + Ok(&Inherited(TestValue(1))) + ); + assert_eq!( + query.get_many(app.world(), [propagate_over, propagatee]), + Ok([&TestValue(2), &TestValue(1)]) + ); + app.world_mut() .commands() - .entity(propagatee) - .remove::(); + .entity(propagate_over) + .remove::>(); app.update(); - assert!(app - .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_err()); + + assert_eq!( + query.get_many(app.world(), [propagate_over, propagatee]), + Ok([&TestValue(1), &TestValue(1)]) + ); } #[test] - fn test_remove_propagated() { + fn test_propagate_over_parent_removed() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); - let propagatee = app + let propagate_over = app .world_mut() - .spawn_empty() - .insert(ChildOf(propagator)) + .spawn(TestValue(2)) + .insert((PropagateOver::::default(), ChildOf(propagator))) .id(); app.update(); - assert!(app - .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_ok()); + + assert_eq!( + query.get_many(app.world(), [propagator, propagate_over]), + Ok([&TestValue(1), &TestValue(2)]) + ); + app.world_mut() .commands() .entity(propagator) .remove::>(); app.update(); - assert!(app - .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_err()); + + assert!(query.get(app.world(), propagator).is_err(),); + assert_eq!(query.get(app.world(), propagate_over), Ok(&TestValue(2))); } #[test] - fn test_propagate_over() { + fn test_orphaned_propagate_over() { let mut app = App::new(); app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagate_over = app .world_mut() .spawn(TestValue(2)) - .insert(ChildOf(propagator)) + .insert((PropagateOver::::default(), ChildOf(propagator))) .id(); let propagatee = app .world_mut() @@ -463,12 +593,20 @@ mod tests { .id(); app.update(); + assert_eq!( - app.world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee), - Ok(&TestValue(1)) + query.get_many(app.world(), [propagate_over, propagatee]), + Ok([&TestValue(2), &TestValue(1)]) ); + + app.world_mut() + .commands() + .entity(propagate_over) + .remove::(); + app.update(); + + assert_eq!(query.get(app.world(), propagate_over), Ok(&TestValue(2))); + assert!(query.get(app.world(), propagatee).is_err()); } #[test] @@ -477,6 +615,8 @@ mod tests { app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagate_stop = app .world_mut() @@ -490,11 +630,46 @@ mod tests { .id(); app.update(); - assert!(app + + assert_eq!(query.get(app.world(), propagate_stop), Ok(&TestValue(1))); + assert!(query.get(app.world(), no_propagatee).is_err()); + } + + #[test] + fn test_remove_propagate_stop() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + + let mut query = app.world_mut().query::<&TestValue>(); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagate_stop = app + .world_mut() + .spawn(PropagateStop::::default()) + .insert(ChildOf(propagator)) + .id(); + let no_propagatee = app .world_mut() - .query::<&TestValue>() - .get(app.world(), no_propagatee) - .is_err()); + .spawn_empty() + .insert(ChildOf(propagate_stop)) + .id(); + + app.update(); + + assert_eq!(query.get(app.world(), propagate_stop), Ok(&TestValue(1))); + assert!(query.get(app.world(), no_propagatee).is_err()); + + app.world_mut() + .commands() + .entity(propagate_stop) + .remove::>(); + app.update(); + + assert_eq!( + query.get_many(app.world(), [propagate_stop, no_propagatee]), + Ok([&TestValue(1), &TestValue(1)]) + ); } #[test] @@ -503,6 +678,8 @@ mod tests { app.add_schedule(Schedule::new(Update)); app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let intermediate = app .world_mut() @@ -516,22 +693,22 @@ mod tests { .id(); app.update(); + assert_eq!( - app.world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee), - Ok(&TestValue(1)) + query.get_many(app.world(), [propagator, intermediate, propagatee]), + Ok([&TestValue(1), &TestValue(1), &TestValue(1)]) ); app.world_mut() .entity_mut(intermediate) .insert(Propagate(TestValue(2))); app.update(); + assert_eq!( app.world_mut() .query::<&TestValue>() - .get(app.world(), propagatee), - Ok(&TestValue(2)) + .get_many(app.world(), [propagator, intermediate, propagatee]), + Ok([&TestValue(1), &TestValue(2), &TestValue(2)]) ); } @@ -546,6 +723,8 @@ mod tests { Update, )); + let mut query = app.world_mut().query::<&TestValue>(); + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); let propagatee = app .world_mut() @@ -554,30 +733,111 @@ mod tests { .id(); app.update(); - assert!(app - .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_err()); + + assert!(query.get(app.world(), propagator).is_err()); + assert!(query.get(app.world(), propagatee).is_err()); // NOTE: changes to the filter condition are not rechecked app.world_mut().entity_mut(propagator).insert(Marker); + app.update(); + + assert!(query.get(app.world(), propagator).is_err()); + assert!(query.get(app.world(), propagatee).is_err()); + + app.world_mut() + .entity_mut(propagator) + .insert(Propagate(TestValue(1))); + app.update(); + + assert_eq!(query.get(app.world(), propagator), Ok(&TestValue(1))); + assert!(query.get(app.world(), propagatee).is_err()); + app.world_mut().entity_mut(propagatee).insert(Marker); app.update(); - assert!(app - .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_err()); + + assert_eq!(query.get(app.world(), propagator), Ok(&TestValue(1))); + assert!(query.get(app.world(), propagatee).is_err()); app.world_mut() .entity_mut(propagator) .insert(Propagate(TestValue(1))); app.update(); - assert!(app + + assert_eq!( + app.world_mut() + .query::<&TestValue>() + .get_many(app.world(), [propagator, propagatee]), + Ok([&TestValue(1), &TestValue(1)]) + ); + } + + #[test] + fn test_removed_propagate_still_inherits() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + + let mut query = app.world_mut().query::<&TestValue>(); + + let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id(); + let propagatee = app .world_mut() - .query::<&TestValue>() - .get(app.world(), propagatee) - .is_ok()); + .spawn(Propagate(TestValue(2))) + .insert(ChildOf(propagator)) + .id(); + + app.update(); + + assert_eq!( + query.get_many(app.world(), [propagator, propagatee]), + Ok([&TestValue(1), &TestValue(2)]) + ); + + app.world_mut() + .commands() + .entity(propagatee) + .remove::>(); + app.update(); + + assert_eq!( + query.get_many(app.world(), [propagator, propagatee]), + Ok([&TestValue(1), &TestValue(1)]) + ); + } + + #[test] + fn test_reparent_respects_stop() { + let mut app = App::new(); + app.add_schedule(Schedule::new(Update)); + app.add_plugins(HierarchyPropagatePlugin::::new(Update)); + + let mut query = app.world_mut().query::<&TestValue>(); + + let propagator = app + .world_mut() + .spawn(( + Propagate(TestValue(1)), + PropagateStop::::default(), + )) + .id(); + let propagatee = app.world_mut().spawn(TestValue(2)).id(); + + app.update(); + + assert_eq!( + query.get_many(app.world(), [propagator, propagatee]), + Ok([&TestValue(1), &TestValue(2)]) + ); + + app.world_mut() + .commands() + .entity(propagatee) + .insert(ChildOf(propagator)); + app.update(); + + assert_eq!( + query.get_many(app.world(), [propagator, propagatee]), + Ok([&TestValue(1), &TestValue(2)]) + ); } } From 80e17cce422bf682949e449123a3f6c7cde68773 Mon Sep 17 00:00:00 2001 From: Saratii <87591431+Saratii@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:51:33 -0600 Subject: [PATCH 051/133] Add optimized is_in_half_space_identity, contains_aabb_identity, and intersects_obb_identity (#21249) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective This adds an optimized version of is_in_half_space, `Frustum::contains_aabb_identity`, `Aabb::is_in_half_space_identity`, and `Frustum::intersects_obb_identity` that takes advantage of how calling with IDENTITY (common) reduces the amount of matrix multiplications. ## Solution Add a specialized function without touching the original that assumes Identity was passed to simplify math. ## Testing I use this function extensively in my own project. I asserted the old function called with identity always returns the same as my new function. I bench marked and profiled the usage in my real application and noticed a 16% speed up on Linux and 20% on windows for contains_aabb. I bench marked intersects_obb_identity and noticed a 39% increase on linux and 38% on windows. Both functions have unit tests that assert calling with identity yields the same result in both versions. --------- Co-authored-by: François Mockers Co-authored-by: IQuick 143 --- crates/bevy_camera/src/primitives.rs | 97 +++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index 1f3abe97ed6f3..db02eb80f96a5 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -133,6 +133,16 @@ impl Aabb { let signed_distance = p_normal.dot(aabb_center_world) + half_space.d(); signed_distance > r } + + /// Optimized version of [`Self::is_in_half_space`] when the AABB is already in world space. + /// Use this when `world_from_local` would be the identity transform. + #[inline] + pub fn is_in_half_space_identity(&self, half_space: &HalfSpace) -> bool { + let p_normal = half_space.normal(); + let r = self.half_extents.abs().dot(p_normal.abs()); + let signed_distance = p_normal.dot(self.center) + half_space.d(); + signed_distance > r + } } impl From for Aabb { @@ -369,7 +379,22 @@ impl Frustum { true } - /// Check if the frustum contains the Axis-Aligned Bounding Box (AABB). + /// Optimized version of [`Frustum::intersects_obb`] + /// where the transform is [`Affine3A::IDENTITY`] and both `intersect_near` and `intersect_far` are `true`. + #[inline] + pub fn intersects_obb_identity(&self, aabb: &Aabb) -> bool { + let aabb_center_world = aabb.center.extend(1.0); + for half_space in self.half_spaces.iter() { + let p_normal = half_space.normal(); + let relative_radius = aabb.half_extents.abs().dot(p_normal.abs()); + if half_space.normal_d().dot(aabb_center_world) + relative_radius <= 0.0 { + return false; + } + } + true + } + + /// Check if the frustum contains the entire Axis-Aligned Bounding Box (AABB). /// Referenced from: [Frustum Culling](https://learnopengl.com/Guest-Articles/2021/Scene/Frustum-Culling) #[inline] pub fn contains_aabb(&self, aabb: &Aabb, world_from_local: &Affine3A) -> bool { @@ -380,6 +405,18 @@ impl Frustum { } true } + + /// Optimized version of [`Self::contains_aabb`] when the AABB is already in world space. + /// Use this when `world_from_local` would be [`Affine3A::IDENTITY`]. + #[inline] + pub fn contains_aabb_identity(&self, aabb: &Aabb) -> bool { + for half_space in &self.half_spaces { + if !aabb.is_in_half_space_identity(half_space) { + return false; + } + } + true + } } pub struct CubeMapFace { @@ -772,4 +809,62 @@ mod tests { ); assert!(!frustum.contains_aabb(&aabb, &model)); } + + #[test] + fn test_identity_optimized_equivalence() { + let cases = vec![ + ( + Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::splat(1.0), + }, + HalfSpace::new(Vec4::new(1.0, 0.0, 0.0, -0.5)), + ), + ( + Aabb { + center: Vec3A::new(2.0, -1.0, 0.5), + half_extents: Vec3A::new(1.0, 2.0, 0.5), + }, + HalfSpace::new(Vec4::new(1.0, 1.0, 1.0, -1.0).normalize()), + ), + ( + Aabb { + center: Vec3A::new(1.0, 1.0, 1.0), + half_extents: Vec3A::ZERO, + }, + HalfSpace::new(Vec4::new(0.0, 0.0, 1.0, -2.0)), + ), + ]; + for (aabb, half_space) in cases { + let general = aabb.is_in_half_space(&half_space, &Affine3A::IDENTITY); + let identity = aabb.is_in_half_space_identity(&half_space); + assert_eq!(general, identity,); + } + } + + #[test] + fn intersects_obb_identity_matches_standard_true_true() { + let frusta = [frustum(), long_frustum(), big_frustum()]; + let aabbs = [ + Aabb { + center: Vec3A::ZERO, + half_extents: Vec3A::new(0.5, 0.5, 0.5), + }, + Aabb { + center: Vec3A::new(1.0, 0.0, 0.5), + half_extents: Vec3A::new(0.9, 0.9, 0.9), + }, + Aabb { + center: Vec3A::new(100.0, 100.0, 100.0), + half_extents: Vec3A::new(1.0, 1.0, 1.0), + }, + ]; + for fr in &frusta { + for aabb in &aabbs { + let standard = fr.intersects_obb(aabb, &Affine3A::IDENTITY, true, true); + let optimized = fr.intersects_obb_identity(aabb); + assert_eq!(standard, optimized); + } + } + } } From 8ca07c4727ee2cde23e7129014208cf96a08a2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?charlotte=20=F0=9F=8C=B8?= Date: Mon, 8 Dec 2025 15:57:25 -0800 Subject: [PATCH 052/133] Add flag to force MSAA writeback (#22066) For Processing, we sometimes need to write out of band to the main view target, which will get clobbered if it's the first camera and writeback doesn't occur. --- crates/bevy_camera/src/camera.rs | 12 +++++------- crates/bevy_camera/src/clear_color.rs | 19 +++++++++++++++++++ crates/bevy_camera/src/lib.rs | 4 ++-- .../bevy_post_process/src/msaa_writeback.rs | 13 +++++++++---- crates/bevy_render/src/camera.rs | 6 +++--- 5 files changed, 38 insertions(+), 16 deletions(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 85280928931dd..d68f4c29b5bbf 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -2,7 +2,7 @@ use crate::primitives::Frustum; use super::{ visibility::{Visibility, VisibleEntities}, - ClearColorConfig, + ClearColorConfig, MsaaWriteback, }; use bevy_asset::Handle; use bevy_derive::Deref; @@ -360,11 +360,9 @@ pub struct Camera { pub target: RenderTarget, /// The [`CameraOutputMode`] for this camera. pub output_mode: CameraOutputMode, - /// If this is enabled, a previous camera exists that shares this camera's render target, and this camera has MSAA enabled, then the previous camera's - /// outputs will be written to the intermediate multi-sampled render target textures for this camera. This enables cameras with MSAA enabled to - /// "write their results on top" of previous camera results, and include them as a part of their render results. This is enabled by default to ensure - /// cameras with MSAA enabled layer their results in the same way as cameras without MSAA enabled by default. - pub msaa_writeback: bool, + /// Controls when MSAA writeback occurs for this camera. + /// See [`MsaaWriteback`] for available options. + pub msaa_writeback: MsaaWriteback, /// The clear color operation to perform on the render target. pub clear_color: ClearColorConfig, /// If set, this camera will be a sub camera of a large view, defined by a [`SubCameraView`]. @@ -380,7 +378,7 @@ impl Default for Camera { computed: Default::default(), target: Default::default(), output_mode: Default::default(), - msaa_writeback: true, + msaa_writeback: MsaaWriteback::default(), clear_color: Default::default(), sub_camera_view: None, } diff --git a/crates/bevy_camera/src/clear_color.rs b/crates/bevy_camera/src/clear_color.rs index aeff7b3428a66..979a9503cec56 100644 --- a/crates/bevy_camera/src/clear_color.rs +++ b/crates/bevy_camera/src/clear_color.rs @@ -22,6 +22,25 @@ pub enum ClearColorConfig { None, } +/// Controls when MSAA writeback occurs for a camera. +/// +/// MSAA writeback copies the previous camera's output into the MSAA sampled texture before +/// rendering, allowing multiple cameras to layer their results when MSAA is enabled. +#[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default, PartialEq, Eq)] +#[reflect(Serialize, Deserialize, Default, Clone)] +pub enum MsaaWriteback { + /// Never perform MSAA writeback for this camera. + Off, + /// Perform MSAA writeback only when this camera is not the first one rendering to the target. + /// This is the default behavior - the first camera has nothing to write back. + #[default] + Auto, + /// Always perform MSAA writeback, even if this is the first camera rendering to the target. + /// This is useful when content has been written directly to the main texture (e.g., via + /// `write_texture`) and needs to be preserved through the MSAA render pass. + Always, +} + /// A [`Resource`] that stores the default color that cameras use to clear the screen between frames. /// /// This color appears as the "background" color for simple apps, diff --git a/crates/bevy_camera/src/lib.rs b/crates/bevy_camera/src/lib.rs index 298b2cde0b411..b8450dcada258 100644 --- a/crates/bevy_camera/src/lib.rs +++ b/crates/bevy_camera/src/lib.rs @@ -34,8 +34,8 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ visibility::{InheritedVisibility, ViewVisibility, Visibility}, - Camera, Camera2d, Camera3d, ClearColor, ClearColorConfig, OrthographicProjection, - PerspectiveProjection, Projection, + Camera, Camera2d, Camera3d, ClearColor, ClearColorConfig, MsaaWriteback, + OrthographicProjection, PerspectiveProjection, Projection, }; } diff --git a/crates/bevy_post_process/src/msaa_writeback.rs b/crates/bevy_post_process/src/msaa_writeback.rs index 859515ff5aee3..0a35088f7f0f4 100644 --- a/crates/bevy_post_process/src/msaa_writeback.rs +++ b/crates/bevy_post_process/src/msaa_writeback.rs @@ -1,4 +1,5 @@ use bevy_app::{App, Plugin}; +use bevy_camera::MsaaWriteback; use bevy_color::LinearRgba; use bevy_core_pipeline::{ blit::{BlitPipeline, BlitPipelineKey}, @@ -135,10 +136,14 @@ fn prepare_msaa_writeback_pipelines( view_targets: Query<(Entity, &ViewTarget, &ExtractedCamera, &Msaa)>, ) { 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 - { + // Determine if we should do MSAA writeback based on the camera's setting + let should_writeback = match camera.msaa_writeback { + MsaaWriteback::Off => false, + MsaaWriteback::Auto => camera.sorted_camera_index_for_target > 0, + MsaaWriteback::Always => true, + }; + + if msaa.samples() > 1 && should_writeback { let key = BlitPipelineKey { texture_format: view_target.main_texture_format(), samples: msaa.samples(), diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index cb5bcb2500bbe..99ccffda26183 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -20,8 +20,8 @@ use bevy_camera::{ primitives::Frustum, visibility::{self, RenderLayers, VisibleEntities}, Camera, Camera2d, Camera3d, CameraMainTextureUsages, CameraOutputMode, CameraUpdateSystems, - ClearColor, ClearColorConfig, Exposure, ManualTextureViewHandle, NormalizedRenderTarget, - Projection, RenderTargetInfo, Viewport, + ClearColor, ClearColorConfig, Exposure, ManualTextureViewHandle, MsaaWriteback, + NormalizedRenderTarget, Projection, RenderTargetInfo, Viewport, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -409,7 +409,7 @@ pub struct ExtractedCamera { pub render_graph: InternedRenderSubGraph, pub order: isize, pub output_mode: CameraOutputMode, - pub msaa_writeback: bool, + pub msaa_writeback: MsaaWriteback, pub clear_color: ClearColorConfig, pub sorted_camera_index_for_target: usize, pub exposure: f32, From fca45967ec6439b17673dad8c7f6854f89082b51 Mon Sep 17 00:00:00 2001 From: Lyndon-Mackay <31716003+Lyndon-Mackay@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:45:11 +1300 Subject: [PATCH 053/133] DespawnOnExit / DespawnOnEnter fix log spam (#21864) if entity was despawned such as in a hierachy # Objective - DespawnOnExit / DespawnOnEnter logs errors with despawning. Most if not all use cases don't care if the item is already despawned. The user is typically saying "Ensure it is despawned" rather then "You should be the one to despawn" - Fixes #21832 ## Solution - Changes the state_scoped code to use try despawn ## Testing - I updated example state_scoped to include children with DespawnOnExit/DespawnOnEnter and added a brief explanation then I ran the examples --------- Co-authored-by: mgi388 <135186256+mgi388@users.noreply.github.com> --- crates/bevy_state/src/state_scoped.rs | 8 ++++++-- examples/ecs/state_scoped.rs | 11 +++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 1d224da1c13ff..ea2acdcf816db 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -67,6 +67,8 @@ where /// Despawns entities marked with [`DespawnOnExit`] when their state no /// longer matches the world state. +/// +/// If the entity has already been despawned no warning will be emitted. pub fn despawn_entities_on_exit_state( mut commands: Commands, mut transitions: MessageReader>, @@ -86,7 +88,7 @@ pub fn despawn_entities_on_exit_state( }; for (entity, binding) in &query { if binding.0 == *exited { - commands.entity(entity).despawn(); + commands.entity(entity).try_despawn(); } } } @@ -133,6 +135,8 @@ pub struct DespawnOnEnter(pub S); /// Despawns entities marked with [`DespawnOnEnter`] when their state /// matches the world state. +/// +/// If the entity has already been despawned no warning will be emitted. pub fn despawn_entities_on_enter_state( mut commands: Commands, mut transitions: MessageReader>, @@ -152,7 +156,7 @@ pub fn despawn_entities_on_enter_state( }; for (entity, binding) in &query { if binding.0 == *entered { - commands.entity(entity).despawn(); + commands.entity(entity).try_despawn(); } } } diff --git a/examples/ecs/state_scoped.rs b/examples/ecs/state_scoped.rs index 8fd4f0a6196fd..ad8d2fac38fbb 100644 --- a/examples/ecs/state_scoped.rs +++ b/examples/ecs/state_scoped.rs @@ -3,6 +3,10 @@ //! //! This pattern is useful for managing menus, levels, or other state-specific //! content that should only exist during certain states. +//! +//! If the entity was already despawned then no error will be logged. This means +//! that you don't have to worry about duplicate [`DespawnOnExit`] and +//! [`DespawnOnEnter`] components deep in your hierarchy. use bevy::prelude::*; @@ -46,6 +50,7 @@ fn on_a_enter(mut commands: Commands) { left: px(0), ..default() }, + (children![DespawnOnExit(GameState::A)]), )); } @@ -65,6 +70,10 @@ fn on_a_exit(mut commands: Commands) { left: px(500), ..default() }, + // You can apply this even when the parent has a state scoped component. + // It is unnecessary but in complex hierarchies it saves you from having to + // mentally track which components are found at the top level. + (children![DespawnOnEnter(GameState::A)]), )); } @@ -84,6 +93,7 @@ fn on_b_enter(mut commands: Commands) { left: px(0), ..default() }, + (children![DespawnOnExit(GameState::B)]), )); } @@ -103,6 +113,7 @@ fn on_b_exit(mut commands: Commands) { left: px(500), ..default() }, + (children![DespawnOnEnter(GameState::B)]), )); } From 8c6e49018d3e11b77933544ff92b03fda63245ad Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:10:18 -0500 Subject: [PATCH 054/133] Remove `bevy_ptr::dangling_with_align()` and inline it (#21822) # Objective - `bevy_ptr::dangling_with_align()` is only used once, in `bevy_ecs`'s `BlobArray::with_capacity()`, and it isn't generally useful outside of the engine's internals. We can remove the function and inline its implementation into its call site. - Additionally, `bevy_ptr::dangling_with_align()` has a TODO comment that was leftover from https://github.com/bevyengine/bevy/pull/15311#discussion_r1768091379, where it was suggested that `dangling_with_align()` should use `without_provenance()`. - `with_addr()`, mentioned in the TODO comment, could also be used, but it's a more roundabout solution. The reason it was mentioned was because the original author thought it would be stabilized before `without_provenance()`. ## Solution - Remove `dangling_with_align()`. - Replace its usage with `NonNull::without_provenance()` (since it is now stable and doesn't require `unsafe` or pointer math) ## Testing - I ran Miri with strict provenance checking enabled to ensure the behavior is maintained. - `MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test` ## But what is this provenance thingy? [The official docs provide a more in-depth explanation](https://doc.rust-lang.org/stable/std/ptr/index.html#provenance), but basically a pointer is more than just a number referring to a memory address. Pointers have _permissions_ associated with them that track: - What set of memory addresses are allowed to be accessed - When the pointer is allowed to access those addresses - If the pointer is allowed to mutate the memory, or just read it These permissions are pointer provenance. They aren't stored at runtime, the Rust compiler doesn't know them at compile time! Miri is the only tool that I know of that tracks provenance, which it uses to ensure pointers adheres to their spatial, temporal, and mutability permissions. That's why I mentioned Miri in the Testing section. :) --- Cargo.toml | 2 +- crates/bevy_ecs/Cargo.toml | 2 +- crates/bevy_ecs/src/error/bevy_error.rs | 32 +++++++++---------- crates/bevy_ecs/src/storage/blob_array.rs | 5 ++- crates/bevy_ptr/src/lib.rs | 11 ------- .../remove_dangling_with_align.md | 14 ++++++++ 6 files changed, 36 insertions(+), 30 deletions(-) create mode 100644 release-content/migration-guides/remove_dangling_with_align.md diff --git a/Cargo.toml b/Cargo.toml index d0b21ce74bbe0..15ae77245973a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["game", "engine", "gamedev", "graphics", "bevy"] license = "MIT OR Apache-2.0" repository = "https://github.com/bevyengine/bevy" documentation = "https://docs.rs/bevy" -rust-version = "1.88.0" +rust-version = "1.89.0" [workspace] resolver = "2" diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 98b9c33bb07c8..14da777c6900d 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["ecs", "game", "bevy"] categories = ["game-engines", "data-structures"] -rust-version = "1.86.0" +rust-version = "1.89.0" [features] default = ["std", "bevy_reflect", "async_executor", "backtrace"] diff --git a/crates/bevy_ecs/src/error/bevy_error.rs b/crates/bevy_ecs/src/error/bevy_error.rs index c290e249b2cfd..352162ae826a1 100644 --- a/crates/bevy_ecs/src/error/bevy_error.rs +++ b/crates/bevy_ecs/src/error/bevy_error.rs @@ -191,10 +191,10 @@ mod tests { // On mac backtraces can start with Backtrace::create let mut skip = false; - if let Some(line) = lines.peek() { - if &line[6..] == "std::backtrace::Backtrace::create" { - skip = true; - } + if let Some(line) = lines.peek() + && &line[6..] == "std::backtrace::Backtrace::create" + { + skip = true; } if skip { @@ -212,10 +212,10 @@ mod tests { let line = lines.next().unwrap(); assert_eq!(&line[6..], expected); let mut skip = false; - if let Some(line) = lines.peek() { - if line.starts_with(" at") { - skip = true; - } + if let Some(line) = lines.peek() + && line.starts_with(" at") + { + skip = true; } if skip { @@ -225,19 +225,19 @@ mod tests { // on linux there is a second call_once let mut skip = false; - if let Some(line) = lines.peek() { - if &line[6..] == "core::ops::function::FnOnce::call_once" { - skip = true; - } + if let Some(line) = lines.peek() + && &line[6..] == "core::ops::function::FnOnce::call_once" + { + skip = true; } if skip { lines.next().unwrap(); } let mut skip = false; - if let Some(line) = lines.peek() { - if line.starts_with(" at") { - skip = true; - } + if let Some(line) = lines.peek() + && line.starts_with(" at") + { + skip = true; } if skip { diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index 3ef2e5a90d007..bab0aad03c443 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -42,7 +42,10 @@ impl BlobArray { ) -> Self { if capacity == 0 { let align = NonZeroUsize::new(item_layout.align()).expect("alignment must be > 0"); - let data = bevy_ptr::dangling_with_align(align); + + // Create a dangling pointer with the given alignment. + let data = NonNull::without_provenance(align); + Self { item_layout, drop: drop_fn, diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index e76fcc9f57250..f59a02496afda 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -12,7 +12,6 @@ use core::{ fmt::{self, Debug, Formatter, Pointer}, marker::PhantomData, mem::{self, ManuallyDrop, MaybeUninit}, - num::NonZeroUsize, ops::{Deref, DerefMut}, ptr::{self, NonNull}, }; @@ -1139,16 +1138,6 @@ impl<'a, T> From<&'a [T]> for ThinSlicePtr<'a, T> { } } -/// Creates a dangling pointer with specified alignment. -/// See [`NonNull::dangling`]. -pub const fn dangling_with_align(align: NonZeroUsize) -> NonNull { - debug_assert!(align.is_power_of_two(), "Alignment must be power of two."); - // SAFETY: The pointer will not be null, since it was created - // from the address of a `NonZero`. - // TODO: use https://doc.rust-lang.org/std/ptr/struct.NonNull.html#method.with_addr once stabilized - unsafe { NonNull::new_unchecked(ptr::null_mut::().wrapping_add(align.get())) } -} - mod private { use core::cell::UnsafeCell; diff --git a/release-content/migration-guides/remove_dangling_with_align.md b/release-content/migration-guides/remove_dangling_with_align.md new file mode 100644 index 0000000000000..71fbd10e859c9 --- /dev/null +++ b/release-content/migration-guides/remove_dangling_with_align.md @@ -0,0 +1,14 @@ +--- +title: Remove `bevy::ptr::dangling_with_align()` +pull_requests: [21822] +--- + +`bevy::ptr::dangling_with_align()` has been removed. Use `NonNull::without_provenance()` instead: + +```rust +// 0.17 +let ptr = dangling_with_align(align); + +// 0.18 +let ptr = NonNull::without_provenance(align); +``` From 8a0367e7eb5306761df85afc8632a3fc0b314d58 Mon Sep 17 00:00:00 2001 From: DuckyBlender Date: Tue, 9 Dec 2025 06:16:59 +0100 Subject: [PATCH 055/133] Add vertical slider support to bevy_ui_widgets slider (#21827) # Objective - Fixes the issue of vertical sliders not being functional. Previously, when creating a vertical slider, the slider drag behavior was still horizontal, meaning dragging left/right would change the value instead of dragging up/down. Additionally, clicking on the slider track was offset and didn't correctly map to the clicked position. ## Solution The slider widget now automatically detects its orientation based on the node's dimensions (`height > width` for vertical, otherwise horizontal) and adjusts its interaction behavior accordingly: 1. **Orientation Detection**: Added automatic detection of slider orientation by comparing `node.size().y` to `node.size().x` in both `slider_on_pointer_down` and `slider_on_drag` functions. 2. **Drag Direction Fix**: - For vertical sliders, the drag calculation now uses the Y-axis (`distance.y`) instead of X-axis - The Y coordinate is properly inverted (since screen Y increases downward) to match expected behavior (dragging up increases value) - Thumb size calculation uses `thumb.size().y` for vertical sliders instead of always using `thumb.size().x` 3. **Click Position Fix**: - Fixed coordinate conversion from Bevy's center-origin coordinate system to top/left-origin coordinates - For vertical sliders: converts `local_pos.y` from `[-height/2, +height/2]` to `[0, height]` before calculating the slider value - Accounts for thumb size offset to center the calculation properly - Inverts the Y coordinate for vertical sliders since Y increases downward 4. **Track Size Calculation**: Uses `node.size().y - thumb_size` for vertical sliders and `node.size().x - thumb_size` for horizontal sliders when calculating the available track space. The changes are backward compatible - horizontal sliders continue to work exactly as before, and the orientation detection is transparent to users of the API. ## Testing - **Manual Testing**: Created test application with both vertical and horizontal sliders to verify: - Vertical sliders respond correctly to vertical drag movements (up = increase, down = decrease) - Horizontal sliders continue to work correctly with horizontal drag movements - Clicking anywhere on the slider track correctly snaps to that position for both orientations - Thumb positioning updates correctly during drag operations - Multiple sliders can coexist without interfering with each other - **Edge Cases Tested**: - Clicking at the very top/bottom of vertical sliders - Clicking at the very left/right of horizontal sliders - Dragging from one extreme to the other - Rapid clicking and dragging interactions **Areas that may need more testing:** - Sliders with non-standard aspect ratios (very wide vertical sliders or very tall horizontal sliders) - Sliders with custom transforms/rotations applied - Sliders in nested UI hierarchies with complex transforms **How reviewers can test:** 1. Create a vertical slider (height > width) and verify: - Dragging up increases the value - Dragging down decreases the value - Clicking anywhere on the track snaps to that exact position 2. Create a horizontal slider (width > height) and verify it still works as before 3. Test with multiple sliders of both orientations simultaneously **Platforms tested:** - macOS (Apple Silicon) ## Showcase ### Before - Vertical sliders were unusable - dragging would move horizontally instead of vertically - Clicking on vertical slider tracks was offset, with clicks near the bottom snapping to the middle instead https://github.com/user-attachments/assets/9bda83e7-f46b-4626-9df3-a558526a8ab2 ### After - Vertical sliders work correctly with intuitive up/down drag behavior - Clicking anywhere on the slider track accurately snaps to the clicked position - Both vertical and horizontal sliders work seamlessly together https://github.com/user-attachments/assets/cca24288-4cbc-4ceb-828f-9ce7a735ade0 ### Code Example ```rust // Vertical slider - now works correctly! commands.spawn(( Node { width: Val::Px(12.0), height: Val::Px(300.0), // height > width = vertical ..default() }, Slider::default(), SliderValue(50.0), SliderRange::new(0.0, 100.0), // ... thumb and track children )); // Horizontal slider - continues to work as before commands.spawn(( Node { width: Val::Px(300.0), height: Val::Px(12.0), // width > height = horizontal ..default() }, Slider::default(), SliderValue(50.0), SliderRange::new(0.0, 100.0), // ... thumb and track children )); ``` The orientation is automatically detected based on the node dimensions - no API changes required! --------- Co-authored-by: Alice Cecile --- Cargo.toml | 12 + crates/bevy_ui_widgets/src/slider.rs | 77 +++++-- examples/README.md | 1 + examples/ui/vertical_slider.rs | 313 +++++++++++++++++++++++++++ 4 files changed, 386 insertions(+), 17 deletions(-) create mode 100644 examples/ui/vertical_slider.rs diff --git a/Cargo.toml b/Cargo.toml index 15ae77245973a..74b0ebe4ae6aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3491,6 +3491,18 @@ description = "Illustrates how to access `winit::window::Window`'s `hittest` fun category = "UI (User Interface)" wasm = false +[[example]] +name = "vertical_slider" +path = "examples/ui/vertical_slider.rs" +doc-scrape-examples = true +required-features = ["experimental_bevy_ui_widgets"] + +[package.metadata.example.vertical_slider] +name = "Vertical Slider" +description = "Simple example showing vertical and horizontal slider widgets with snap behavior and value labels" +category = "UI (User Interface)" +wasm = true + [[example]] name = "font_atlas_debug" path = "examples/ui/font_atlas_debug.rs" diff --git a/crates/bevy_ui_widgets/src/slider.rs b/crates/bevy_ui_widgets/src/slider.rs index d19e344b29e4e..453aaa12e65ed 100644 --- a/crates/bevy_ui_widgets/src/slider.rs +++ b/crates/bevy_ui_widgets/src/slider.rs @@ -264,22 +264,49 @@ pub(crate) fn slider_on_pointer_down( return; } + // Detect orientation: vertical if height > width + let is_vertical = node.size().y > node.size().x; + // Find thumb size by searching descendants for the first entity with SliderThumb let thumb_size = q_children .iter_descendants(press.entity) - .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .find_map(|child_id| { + q_thumb.get(child_id).ok().map(|thumb| { + if is_vertical { + thumb.size().y + } else { + thumb.size().x + } + }) + }) .unwrap_or(0.0); // Detect track click. let local_pos = transform.try_inverse().unwrap().transform_point2( press.pointer_location.position * node_target.scale_factor() / ui_scale.0, ); - let track_width = node.size().x - thumb_size; + let track_size = if is_vertical { + node.size().y - thumb_size + } else { + node.size().x - thumb_size + }; + // Avoid division by zero - let click_val = if track_width > 0. { - local_pos.x * range.span() / track_width + range.center() + let click_val = if track_size > 0. { + if is_vertical { + // For vertical sliders: bottom-to-top (0 at bottom, max at top) + // local_pos.y ranges from -height/2 (top) to +height/2 (bottom) + let y_from_bottom = (node.size().y / 2.0) - local_pos.y; + let adjusted_y = y_from_bottom - thumb_size / 2.0; + adjusted_y * range.span() / track_size + range.start() + } else { + // For horizontal sliders: convert from center-origin to left-origin + let x_from_left = local_pos.x + node.size().x / 2.0; + let adjusted_x = x_from_left - thumb_size / 2.0; + adjusted_x * range.span() / track_size + range.start() + } } else { - 0. + range.center() }; // Compute new value from click position @@ -330,7 +357,6 @@ pub(crate) fn slider_on_drag( mut event: On>, mut q_slider: Query< ( - &SliderValue, &ComputedNode, &SliderRange, Option<&SliderPrecision>, @@ -345,23 +371,42 @@ pub(crate) fn slider_on_drag( mut commands: Commands, ui_scale: Res, ) { - if let Ok((value, node, range, precision, transform, drag, disabled)) = - q_slider.get_mut(event.entity) + if let Ok((node, range, precision, transform, drag, disabled)) = q_slider.get_mut(event.entity) { event.propagate(false); if drag.dragging && !disabled { + // Detect orientation: vertical if height > width + let is_vertical = node.size().y > node.size().x; + let mut distance = event.distance / ui_scale.0; distance.y *= -1.; let distance = transform.transform_vector2(distance); + // Find thumb size by searching descendants for the first entity with SliderThumb let thumb_size = q_children .iter_descendants(event.entity) - .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .find_map(|child_id| { + q_thumb.get(child_id).ok().map(|thumb| { + if is_vertical { + thumb.size().y + } else { + thumb.size().x + } + }) + }) .unwrap_or(0.0); - let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); + + let slider_size = if is_vertical { + ((node.size().y - thumb_size) * node.inverse_scale_factor).max(1.0) + } else { + ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0) + }; + + let drag_distance = if is_vertical { distance.y } else { distance.x }; + let span = range.span(); let new_value = if span > 0. { - drag.offset + (distance.x * span) / slider_width + drag.offset + (drag_distance * span) / slider_size } else { range.start() + span * 0.5 }; @@ -371,12 +416,10 @@ pub(crate) fn slider_on_drag( .unwrap_or(new_value), ); - if rounded_value != value.0 { - commands.trigger(ValueChange { - source: event.entity, - value: rounded_value, - }); - } + commands.trigger(ValueChange { + source: event.entity, + value: rounded_value, + }); } } } diff --git a/examples/README.md b/examples/README.md index ab7402ee46a9c..e119d412eb454 100644 --- a/examples/README.md +++ b/examples/README.md @@ -595,6 +595,7 @@ Example | Description [UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI [UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements. [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements +[Vertical Slider](../examples/ui/vertical_slider.rs) | Simple example showing vertical and horizontal slider widgets with snap behavior and value labels [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support [Virtual Keyboard](../examples/ui/virtual_keyboard.rs) | Example demonstrating a virtual keyboard widget diff --git a/examples/ui/vertical_slider.rs b/examples/ui/vertical_slider.rs new file mode 100644 index 0000000000000..66dcccbad229c --- /dev/null +++ b/examples/ui/vertical_slider.rs @@ -0,0 +1,313 @@ +//! Simple example showing vertical and horizontal slider widgets with snap behavior and value labels + +use bevy::{ + input_focus::{ + tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, + InputDispatchPlugin, + }, + picking::hover::Hovered, + prelude::*, + ui_widgets::{ + observe, slider_self_update, CoreSliderDragState, Slider, SliderRange, SliderThumb, + SliderValue, TrackClick, UiWidgetsPlugins, + }, +}; + +const SLIDER_TRACK: Color = Color::srgb(0.05, 0.05, 0.05); +const SLIDER_THUMB: Color = Color::srgb(0.35, 0.75, 0.35); + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + UiWidgetsPlugins, + InputDispatchPlugin, + TabNavigationPlugin, + )) + .add_systems(Startup, setup) + .add_systems(Update, (update_slider_visuals, update_value_labels)) + .run(); +} + +#[derive(Component)] +struct ValueLabel(Entity); + +#[derive(Component)] +struct DemoSlider; + +#[derive(Component)] +struct DemoSliderThumb; + +#[derive(Component)] +struct VerticalSlider; + +fn setup(mut commands: Commands, assets: Res) { + commands.spawn(Camera2d); + + commands + .spawn(( + Node { + width: percent(100), + height: percent(100), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + display: Display::Flex, + flex_direction: FlexDirection::Row, + column_gap: px(50), + ..default() + }, + TabGroup::default(), + )) + .with_children(|parent| { + // Vertical slider + parent + .spawn(Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + row_gap: px(10), + ..default() + }) + .with_children(|parent| { + parent.spawn(( + Text::new("Vertical"), + TextFont { + font: assets.load("fonts/FiraSans-Bold.ttf"), + font_size: 20.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )); + + let label_id = parent + .spawn(( + Text::new("50"), + TextFont { + font: assets.load("fonts/FiraSans-Bold.ttf"), + font_size: 24.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )) + .id(); + + parent.spawn(( + vertical_slider(), + ValueLabel(label_id), + observe(slider_self_update), + )); + }); + + // Horizontal slider + parent + .spawn(Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + align_items: AlignItems::Center, + row_gap: px(10), + ..default() + }) + .with_children(|parent| { + parent.spawn(( + Text::new("Horizontal"), + TextFont { + font: assets.load("fonts/FiraSans-Bold.ttf"), + font_size: 20.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )); + + let label_id = parent + .spawn(( + Text::new("50"), + TextFont { + font: assets.load("fonts/FiraSans-Bold.ttf"), + font_size: 24.0, + ..default() + }, + TextColor(Color::srgb(0.9, 0.9, 0.9)), + )) + .id(); + + parent.spawn(( + horizontal_slider(), + ValueLabel(label_id), + observe(slider_self_update), + )); + }); + }); +} + +fn vertical_slider() -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Center, + align_items: AlignItems::Stretch, + column_gap: px(4), + width: px(12), + height: px(200), + ..default() + }, + DemoSlider, + VerticalSlider, + Hovered::default(), + Slider { + track_click: TrackClick::Snap, + }, + SliderValue(50.0), + SliderRange::new(0.0, 100.0), + TabIndex(0), + Children::spawn(( + Spawn(( + Node { + width: px(6), + border_radius: BorderRadius::all(px(3)), + ..default() + }, + BackgroundColor(SLIDER_TRACK), + )), + Spawn(( + Node { + display: Display::Flex, + position_type: PositionType::Absolute, + top: px(12), + bottom: px(0), + left: px(0), + right: px(0), + ..default() + }, + children![( + DemoSliderThumb, + SliderThumb, + Node { + display: Display::Flex, + width: px(12), + height: px(12), + position_type: PositionType::Absolute, + bottom: percent(0), + border_radius: BorderRadius::MAX, + ..default() + }, + BackgroundColor(SLIDER_THUMB), + )], + )), + )), + ) +} + +fn horizontal_slider() -> impl Bundle { + ( + Node { + display: Display::Flex, + flex_direction: FlexDirection::Column, + justify_content: JustifyContent::Center, + align_items: AlignItems::Stretch, + column_gap: px(4), + height: px(12), + width: px(200), + ..default() + }, + DemoSlider, + Hovered::default(), + Slider { + track_click: TrackClick::Snap, + }, + SliderValue(50.0), + SliderRange::new(0.0, 100.0), + TabIndex(0), + Children::spawn(( + Spawn(( + Node { + height: px(6), + border_radius: BorderRadius::all(px(3)), + ..default() + }, + BackgroundColor(SLIDER_TRACK), + )), + Spawn(( + Node { + display: Display::Flex, + position_type: PositionType::Absolute, + left: px(0), + right: px(12), + top: px(0), + bottom: px(0), + ..default() + }, + children![( + DemoSliderThumb, + SliderThumb, + Node { + display: Display::Flex, + width: px(12), + height: px(12), + position_type: PositionType::Absolute, + left: percent(0), + border_radius: BorderRadius::MAX, + ..default() + }, + BackgroundColor(SLIDER_THUMB), + )], + )), + )), + ) +} + +fn update_slider_visuals( + sliders: Query< + ( + Entity, + &SliderValue, + &SliderRange, + &Hovered, + &CoreSliderDragState, + Has, + ), + ( + Or<( + Changed, + Changed, + Changed, + )>, + With, + ), + >, + children: Query<&Children>, + mut thumbs: Query<(&mut Node, &mut BackgroundColor, Has), Without>, +) { + for (slider_ent, value, range, hovered, drag_state, is_vertical) in sliders.iter() { + for child in children.iter_descendants(slider_ent) { + if let Ok((mut thumb_node, mut thumb_bg, is_thumb)) = thumbs.get_mut(child) + && is_thumb + { + let position = range.thumb_position(value.0) * 100.0; + if is_vertical { + thumb_node.bottom = percent(position); + } else { + thumb_node.left = percent(position); + } + + let is_active = hovered.0 | drag_state.dragging; + thumb_bg.0 = if is_active { + SLIDER_THUMB.lighter(0.3) + } else { + SLIDER_THUMB + }; + } + } + } +} + +fn update_value_labels( + sliders: Query<(&SliderValue, &ValueLabel), (Changed, With)>, + mut texts: Query<&mut Text>, +) { + for (value, label) in sliders.iter() { + if let Ok(mut text) = texts.get_mut(label.0) { + **text = format!("{:.0}", value.0); + } + } +} From 1a9fee09fa6ba1b65c716d13bf33ce7f93beb547 Mon Sep 17 00:00:00 2001 From: newDINO <147262794+newDINO@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:20:10 +0800 Subject: [PATCH 056/133] Fix shader compilation fail when using visibility range (#21879) # Objective When using both deferred rendering and visibility range, shader `pbr.wgsl` can't be compiled because `pbr_functions` is not imported. The issue can be reproduced by using `DefaultOpaqueRendererMethod::deferred()` and adding `DeferredPrepass` to camera at the visibility_range example. ## Solution Import `pbr_functions` when using `PREPASS_PIPELINE`. ## Testing I tested on visibility_range and deferred_rendering example. However, importing something usually doesn't break anything. --------- Co-authored-by: Nico Zweifel <34443492+NicoZweifel@users.noreply.github.com> --- crates/bevy_pbr/src/render/pbr.wgsl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 1722ab9d91940..e4d8fe63791d7 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -13,12 +13,15 @@ #else #import bevy_pbr::{ forward_io::{VertexOutput, FragmentOutput}, - pbr_functions, pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing}, pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT, } #endif +#ifdef VISIBILITY_RANGE_DITHER +#import bevy_pbr::pbr_functions::visibility_range_dither; +#endif + #ifdef MESHLET_MESH_MATERIAL_PASS #import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output #endif @@ -50,7 +53,7 @@ fn fragment( // If we're in the crossfade section of a visibility range, conditionally // discard the fragment according to the visibility pattern. #ifdef VISIBILITY_RANGE_DITHER - pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither); + visibility_range_dither(in.position, in.visibility_range_dither); #endif #ifdef FORWARD_DECAL From ad48f39348a290f110845d77e83c066f8a1c64a0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 9 Dec 2025 11:28:39 +0000 Subject: [PATCH 057/133] Skip picking for UI nodes if there is no pointer (#22074) # Objective `ui_picking` always iterates through all the uinodes even if no hits are possible because there's no pointer above the current camera. ## Solution Early continue if no pointers are found on the current camera. --- crates/bevy_ui/src/picking_backend.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 99f00d5594f19..189de0a4d365a 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -174,7 +174,9 @@ pub fn ui_picking( continue; }; - let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); + let Some(pointers_on_this_cam) = pointer_pos_by_camera.get(&camera_entity) else { + continue; + }; // Reverse the iterator to traverse the tree from closest nodes to furthest for node_entity in uinodes.iter().rev().cloned() { @@ -203,8 +205,7 @@ pub fn ui_picking( // Find the normalized cursor position relative to the node. // (±0., 0.) is the center with the corners at points (±0.5, ±0.5). // Coordinates are relative to the entire node, not just the visible region. - for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) - { + for (pointer_id, cursor_position) in pointers_on_this_cam.iter() { if node.node.contains_point(*node.transform, *cursor_position) && clip_check_recursive( *cursor_position, From 6684682f290024c2a022f336f6445eb737115e87 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 9 Dec 2025 03:33:18 -0800 Subject: [PATCH 058/133] remove empty_bind_group_layout (#22068) # Objective - Now that we have `BindGroupLayoutDescriptor`, we don't need this ugly hack ## Solution - Remove the ugly hack --- crates/bevy_render/src/lib.rs | 4 +--- .../src/render_resource/bind_group_layout.rs | 20 +------------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index b1e7837b3ec04..2325139a0af98 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -81,7 +81,7 @@ use crate::{ gpu_readback::GpuReadbackPlugin, mesh::{MeshRenderAssetPlugin, RenderMesh}, render_asset::prepare_assets, - render_resource::{init_empty_bind_group_layout, PipelineCache}, + render_resource::PipelineCache, renderer::{render_system, RenderAdapterInfo}, settings::RenderCreation, storage::StoragePlugin, @@ -388,8 +388,6 @@ impl Plugin for RenderPlugin { Render, reset_render_asset_bytes_per_frame.in_set(RenderSystems::Cleanup), ); - - render_app.add_systems(RenderStartup, init_empty_bind_group_layout); } } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout.rs b/crates/bevy_render/src/render_resource/bind_group_layout.rs index fb52cedc2490a..3b8cc000802eb 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout.rs @@ -1,6 +1,4 @@ -use crate::{define_atomic_id, renderer::RenderDevice, renderer::WgpuWrapper}; -use bevy_ecs::system::Res; -use bevy_platform::sync::OnceLock; +use crate::{define_atomic_id, renderer::WgpuWrapper}; use core::ops::Deref; define_atomic_id!(BindGroupLayoutId); @@ -63,19 +61,3 @@ impl Deref for BindGroupLayout { &self.value } } - -static EMPTY_BIND_GROUP_LAYOUT: OnceLock = OnceLock::new(); - -pub(crate) fn init_empty_bind_group_layout(render_device: Res) { - let layout = render_device.create_bind_group_layout(Some("empty_bind_group_layout"), &[]); - EMPTY_BIND_GROUP_LAYOUT - .set(layout) - .expect("init_empty_bind_group_layout was called more than once"); -} - -pub fn empty_bind_group_layout() -> BindGroupLayout { - EMPTY_BIND_GROUP_LAYOUT - .get() - .expect("init_empty_bind_group_layout was not called") - .clone() -} From 5ffc1d6e3e18fcc31e14f3e84ca656d284efba62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B8holm?= Date: Tue, 9 Dec 2025 19:05:39 +0100 Subject: [PATCH 059/133] Fix undefined behavior in screenshots on WASM (#22077) # Objective Currently the screenshot example on https://bevy.org/examples/window/screenshot/ gives invalid PNG files when I tried it. Looking at them they have some of the right bytes, but also a bunch of garbage. The example works on native. I narrowed it down to the one unsafe block in sight (thanks Rust!). I don't fully understand why the code stopped working or if it was ever valid, but in the meantime since it was written a safe API has been added, and using it made the problem go away. ## Solution Bump `js-sys`, use the safe API. ## Testing I ran the example with `bevy run --example screenshot web` --- crates/bevy_render/Cargo.toml | 2 +- crates/bevy_render/src/view/window/screenshot.rs | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 7b4fa6259f64b..6edeec51654c0 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -125,7 +125,7 @@ send_wrapper = { version = "0.6.0" } proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -js-sys = "0.3" +js-sys = "0.3.83" web-sys = { version = "0.3.67", features = [ 'Blob', 'Document', diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 638d3cf7c6b68..4aba6c6cee791 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -151,11 +151,13 @@ pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On let mut image_buffer = std::io::Cursor::new(Vec::new()); img.write_to(&mut image_buffer, format) .map_err(|e| JsValue::from_str(&format!("{e}")))?; - // SAFETY: `image_buffer` only exist in this closure, and is not used after this line - let parts = js_sys::Array::of1(&unsafe { - js_sys::Uint8Array::view(image_buffer.into_inner().as_bytes()) - .into() - }); + + let parts = js_sys::Array::of1( + &js_sys::Uint8Array::new_from_slice( + image_buffer.into_inner().as_bytes(), + ) + .into(), + ); let blob = web_sys::Blob::new_with_u8_array_sequence(&parts)?; let url = web_sys::Url::create_object_url_with_blob(&blob)?; let window = web_sys::window().unwrap(); From 617828cd1f33332fd4949abd7c26fc827ec9de71 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 9 Dec 2025 18:06:59 +0000 Subject: [PATCH 060/133] Simplify `ui_picking`'s camera search (#22075) # Objective In `ui_picking` the camera_query iterator is run through multiple filters and maps, which could be replaced by a single filter. Also camera_query retrieves the `Camera` component but then it is thrown away and queried for again. ## Solution * Just collapse all the maps and filters into one `filter`. * Only query for `Camera` once and keep the result, remove the second query. --- crates/bevy_ui/src/picking_backend.rs | 29 +++++++++------------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 189de0a4d365a..d3ef8abd80d0f 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -119,25 +119,16 @@ pub fn ui_picking( { // This pointer is associated with a render target, which could be used by multiple // cameras. We want to ensure we return all cameras with a matching target. - for camera in camera_query - .iter() - .filter(|(_, _, cam_can_pick)| !settings.require_markers || *cam_can_pick) - .map(|(entity, camera, _)| { - ( - entity, - camera.target.normalize(primary_window.single().ok()), - ) - }) - .filter_map(|(entity, target)| Some(entity).zip(target)) - .filter(|(_entity, target)| target == &pointer_location.target) - .map(|(cam_entity, _target)| cam_entity) - { - let Ok((_, camera_data, _)) = camera_query.get(camera) else { - continue; - }; + for (entity, camera, _) in camera_query.iter().filter(|(_, camera, cam_can_pick)| { + (!settings.require_markers || *cam_can_pick) + && camera + .target + .normalize(primary_window.single().ok()) + .is_some_and(|target| target == pointer_location.target) + }) { let mut pointer_pos = - pointer_location.position * camera_data.target_scaling_factor().unwrap_or(1.); - if let Some(viewport) = camera_data.physical_viewport_rect() { + pointer_location.position * camera.target_scaling_factor().unwrap_or(1.); + if let Some(viewport) = camera.physical_viewport_rect() { if !viewport.as_rect().contains(pointer_pos) { // The pointer is outside the viewport, skip it continue; @@ -145,7 +136,7 @@ pub fn ui_picking( pointer_pos -= viewport.min.as_vec2(); } pointer_pos_by_camera - .entry(camera) + .entry(entity) .or_default() .insert(pointer_id, pointer_pos); } From 695d9d4ef54ee6d74a089f3d76cfbfddcb8cf007 Mon Sep 17 00:00:00 2001 From: panpanpro888 <77934024+panpanpro888@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:14:01 +0200 Subject: [PATCH 061/133] Implement `Clone` and `Copy` for bevy_ptr's `ConstNonNull` for parity with `NonNull` (#22078) # Objective Fixes #22059 ## Solution Just `#[derive(Clone, Copy)]` on `ConstNonNull` since it`s just a wrapper around `NonNull` which implements them ## Testing Just tested if it compiles and functions as it should --- crates/bevy_ptr/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index f59a02496afda..ab05449566d0c 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -161,6 +161,7 @@ mod sealed { /// A newtype around [`NonNull`] that only allows conversion to read-only borrows or pointers. /// /// This type can be thought of as the `*const T` to [`NonNull`]'s `*mut T`. +#[derive(Clone, Copy)] #[repr(transparent)] pub struct ConstNonNull(NonNull); From 185712fbeff61f9b5f10dddae38d25768e709353 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 9 Dec 2025 10:14:55 -0800 Subject: [PATCH 062/133] Add support for normal maps, metallic-roughness maps, and emissive maps to clustered decals. (#22039) This commit expands the number of textures associated with each clustered decal from 1 to 4. The additional 3 textures apply normal maps, metallic-roughness maps, and emissive maps respectively to the surfaces onto which decals are projected. Normal maps are combined using the [*Whiteout* blending method] from SIGGRAPH 2007. This approach was chosen because, subjectively, it appeared better than the more complex [*reoriented normal mapping* (RNM)] approach. Additionally, *Whiteout* normal map blending is commutative and associative, unlike RNM, which is a useful property for our decals, which are currently applied in an unspecified order. (The fact that the order in which our decals are applied is unspecified is unfortunate, but is a long-standing issue and should probably be fixed in a followup.) In particular, commutativity is desirable because otherwise one must specify which normal map is the *base* normal map and which normal map is the *detail* normal map, but that's not a policy decision that Bevy can unconditionally make, as decals aren't necessary more detailed than the base normal map. (For instance, consider a bullet hole decal embedded in a wall with a subtle rough texture; one might reasonably argue that the base material's normal map is the detail map and the bullet hole is the base map, even though the bullet hole's normal map comes from a decal.) Note that, with a custom material shader, it's possible for application code to use the decal images for arbitrary other purposes. For example, with a custom shader an application might use the metallic-roughness map as a clearcoat map instead if it has no need for a metallic-roughness map on a decal. And, of course, a custom material shader could adopt RNM blending for decals if it wishes. A new example, `clustered_decal_maps`, has been added. This example demonstrates the new maps by spawning clustered decals with maps randomly over time and projecting them onto a wall. Screenshot 2025-12-05 095953 --- Cargo.toml | 12 + assets/shaders/custom_clustered_decal.wgsl | 8 +- crates/bevy_light/src/cluster/mod.rs | 59 ++- crates/bevy_pbr/src/decal/clustered.rs | 141 +++++- crates/bevy_pbr/src/decal/clustered.wgsl | 142 ++++-- .../bevy_pbr/src/render/mesh_view_types.wgsl | 6 +- crates/bevy_pbr/src/render/pbr.wgsl | 8 +- crates/bevy_pbr/src/render/utils.wgsl | 6 + examples/3d/clustered_decal_maps.rs | 445 ++++++++++++++++++ examples/3d/clustered_decals.rs | 8 +- examples/README.md | 1 + typos.toml | 1 + 12 files changed, 760 insertions(+), 77 deletions(-) create mode 100644 examples/3d/clustered_decal_maps.rs diff --git a/Cargo.toml b/Cargo.toml index 74b0ebe4ae6aa..b7ab3efcdbd2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4900,3 +4900,15 @@ name = "Pan Camera" description = "Example Pan-Camera Styled Camera Controller for 2D scenes" category = "Camera" wasm = true + +[[example]] +name = "clustered_decal_maps" +path = "examples/3d/clustered_decal_maps.rs" +doc-scrape-examples = true +required-features = ["pbr_clustered_decals", "https"] + +[package.metadata.example.clustered_decal_maps] +name = "Clustered Decal Maps" +description = "Demonstrates normal and metallic-roughness maps of decals" +category = "3D Rendering" +wasm = false diff --git a/assets/shaders/custom_clustered_decal.wgsl b/assets/shaders/custom_clustered_decal.wgsl index 6aaf408097ca9..13f404cebb262 100644 --- a/assets/shaders/custom_clustered_decal.wgsl +++ b/assets/shaders/custom_clustered_decal.wgsl @@ -22,11 +22,7 @@ fn fragment( pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); // Apply the normal decals. - pbr_input.material.base_color = clustered::apply_decal_base_color( - in.world_position.xyz, - in.position.xy, - pbr_input.material.base_color - ); + clustered::apply_decals(&pbr_input); // Here we tint the color based on the tag of the decal. // We could optionally do other things, such as adjust the normal based on a normal map. @@ -42,7 +38,7 @@ fn fragment( ); while (clustered::clustered_decal_iterator_next(&decal_iterator)) { var decal_base_color = textureSampleLevel( - mesh_view_bindings::clustered_decal_textures[decal_iterator.texture_index], + mesh_view_bindings::clustered_decal_textures[decal_iterator.base_color_texture_index], mesh_view_bindings::clustered_decal_sampler, decal_iterator.uv, 0.0 diff --git a/crates/bevy_light/src/cluster/mod.rs b/crates/bevy_light/src/cluster/mod.rs index 92f1c5723eebd..17e3f1e418195 100644 --- a/crates/bevy_light/src/cluster/mod.rs +++ b/crates/bevy_light/src/cluster/mod.rs @@ -148,26 +148,71 @@ pub struct ClusterableObjectCounts { /// An object that projects a decal onto surfaces within its bounds. /// /// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It -/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus -/// you may find [`Transform::looking_at`] useful). +/// projects its images onto surfaces in the -Z direction (thus you may find +/// [`Transform::looking_at`] useful). +/// +/// Each decal may project any of a base color texture, a normal map, a +/// metallic/roughness map, and/or a texture that specifies emissive light. In +/// addition, you may associate an arbitrary integer [`Self::tag`] with each +/// clustered decal, which Bevy doesn't use, but that you can use in your +/// shaders in order to associate application-specific data with your decals. /// /// Clustered decals are the highest-quality types of decals that Bevy supports, /// but they require bindless textures. This means that they presently can't be /// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used /// with forward or deferred rendering and don't require a prepass. -#[derive(Component, Debug, Clone, Reflect)] -#[reflect(Component, Debug, Clone)] +#[derive(Component, Debug, Clone, Default, Reflect)] +#[reflect(Component, Debug, Clone, Default)] #[require(Transform, Visibility, VisibilityClass)] #[component(on_add = visibility::add_visibility_class::)] pub struct ClusteredDecal { - /// The image that the clustered decal projects. + /// The image that the clustered decal projects onto the base color of the + /// surface material. /// /// This must be a 2D image. If it has an alpha channel, it'll be alpha /// blended with the underlying surface and/or other decals. All decal /// images in the scene must use the same sampler. - pub image: Handle, + pub base_color_texture: Option>, + + /// The normal map that the clustered decal projects onto surfaces. + /// + /// Bevy uses the *Whiteout* method to combine normal maps from decals with + /// any normal map that the surface has, as described in the + /// [*Blending in Detail* article]. + /// + /// Note that the normal map must be three-channel and must be in OpenGL + /// format, not DirectX format. That is, the green channel must point up, + /// not down. + /// + /// [*Blending in Detail* article]: https://blog.selfshadow.com/publications/blending-in-detail/ + pub normal_map_texture: Option>, + + /// The metallic-roughness map that the clustered decal projects onto + /// surfaces. + /// + /// Metallic and roughness PBR parameters are blended onto the base surface + /// using the alpha channel of the base color. + /// + /// Metallic is expected to be in the blue channel, while roughness is + /// expected to be in the green channel, following glTF conventions. + pub metallic_roughness_texture: Option>, + + /// The emissive map that the clustered decal projects onto surfaces. + /// + /// Including this texture effectively causes the decal to glow. The + /// emissive component is blended onto the surface according to the alpha + /// channel. + pub emissive_texture: Option>, - /// An application-specific tag you can use for any purpose you want. + /// An application-specific tag you can use for any purpose you want, in + /// conjunction with a custom shader. + /// + /// This value is exposed to the shader via the iterator API + /// (`bevy_pbr::decal::clustered::clustered_decal_iterator_new` and + /// `bevy_pbr::decal::clustered::clustered_decal_iterator_next`). + /// + /// For example, you might use the tag to restrict the set of surfaces to + /// which a decal can be rendered. /// /// See the `clustered_decals` example for an example of use. pub tag: u32, diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index ad32c7f5715b8..1acbd2fbf9dad 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -9,15 +9,19 @@ //! used on WebGL 2 or WebGPU. Bevy's clustered decals can be used //! with forward or deferred rendering and don't require a prepass. //! -//! On their own, clustered decals only project the base color of a texture. You -//! can, however, use the built-in *tag* field to customize the appearance of a -//! clustered decal arbitrarily. See the documentation in `clustered.wgsl` for -//! more information and the `clustered_decals` example for an example of use. +//! Each clustered decal may contain up to 4 textures. By default, the 4 +//! textures correspond to the base color, a normal map, a metallic-roughness +//! map, and an emissive map respectively. However, with a custom shader, you +//! can use these 4 textures for whatever you wish. Additionally, you can use +//! the built-in *tag* field to store additional application-specific data; by +//! reading the tag in the shader, you can modify the appearance of a clustered +//! decal arbitrarily. See the documentation in `clustered.wgsl` for more +//! information and the `clustered_decals` example for an example of use. use core::{num::NonZero, ops::Deref}; use bevy_app::{App, Plugin}; -use bevy_asset::AssetId; +use bevy_asset::{AssetId, Handle}; use bevy_camera::visibility::ViewVisibility; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -50,6 +54,9 @@ use bytemuck::{Pod, Zeroable}; use crate::{binding_arrays_are_usable, prepare_lights, GlobalClusterableObjectMeta}; +/// The number of textures that can be associated with each clustered decal. +const IMAGES_PER_DECAL: usize = 4; + /// A plugin that adds support for clustered decals. /// /// In environments where bindless textures aren't available, clustered decals @@ -66,7 +73,7 @@ pub struct RenderClusteredDecals { /// Maps a decal image to the shader binding array. /// /// [`Self::binding_index_to_textures`] holds the inverse mapping. - texture_to_binding_index: HashMap, u32>, + texture_to_binding_index: HashMap, i32>, /// The information concerning each decal that we provide to the shader. decals: Vec, /// Maps the [`bevy_render::sync_world::RenderEntity`] of each decal to the @@ -87,18 +94,22 @@ impl RenderClusteredDecals { pub fn insert_decal( &mut self, entity: Entity, - image: &AssetId, + images: [Option>; IMAGES_PER_DECAL], local_from_world: Mat4, tag: u32, ) { - let image_index = self.get_or_insert_image(image); + let image_indices = images.map(|maybe_image_id| match maybe_image_id { + Some(ref image_id) => self.get_or_insert_image(image_id), + None => -1, + }); let decal_index = self.decals.len(); self.decals.push(RenderClusteredDecal { local_from_world, - image_index, + image_indices, tag, pad_a: 0, pad_b: 0, + pad_c: 0, }); self.entity_to_decal_index.insert(entity, decal_index); } @@ -183,14 +194,23 @@ pub struct RenderClusteredDecal { /// The shader uses this in order to back-transform world positions into /// model space. local_from_world: Mat4, - /// The index of the decal texture in the binding array. - image_index: u32, + /// The index of each decal texture in the binding array. + /// + /// These are in the order of the base color texture, the normal map + /// texture, the metallic-roughness map texture, and finally the emissive + /// texture. + /// + /// If the decal doesn't have a texture assigned to a slot, the index at + /// that slot will be -1. + image_indices: [i32; 4], /// A custom tag available for application-defined purposes. tag: u32, /// Padding. pad_a: u32, /// Padding. pad_b: u32, + /// Padding. + pad_c: u32, } /// Extracts decals from the main world into the render world. @@ -232,58 +252,129 @@ pub fn extract_decals( // Clear out the `RenderDecals` in preparation for a new frame. render_decals.clear(); + extract_clustered_decals(&decals, &mut render_decals); + extract_spot_light_textures(&spot_light_textures, &mut render_decals); + extract_point_light_textures(&point_light_textures, &mut render_decals); + extract_directional_light_textures(&directional_light_textures, &mut render_decals); +} + +/// Extracts all clustered decals and light textures from the scene and transfers +/// them to the render world. +fn extract_clustered_decals( + decals: &Extract< + Query<( + RenderEntity, + &ClusteredDecal, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { // Loop over each decal. - for (decal_entity, clustered_decal, global_transform, view_visibility) in &decals { + for (decal_entity, clustered_decal, global_transform, view_visibility) in decals { // If the decal is invisible, skip it. if !view_visibility.get() { continue; } + // Insert the decal, grabbing the ID of every associated texture as we + // do. render_decals.insert_decal( decal_entity, - &clustered_decal.image.id(), + [ + clustered_decal.base_color_texture.as_ref().map(Handle::id), + clustered_decal.normal_map_texture.as_ref().map(Handle::id), + clustered_decal + .metallic_roughness_texture + .as_ref() + .map(Handle::id), + clustered_decal.emissive_texture.as_ref().map(Handle::id), + ], global_transform.affine().inverse().into(), clustered_decal.tag, ); } +} - for (decal_entity, texture, global_transform, view_visibility) in &spot_light_textures { - // If the decal is invisible, skip it. +/// Extracts all textures from spot lights from the main world to the render +/// world as clustered decals. +fn extract_spot_light_textures( + spot_light_textures: &Extract< + Query<( + RenderEntity, + &SpotLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + for (decal_entity, texture, global_transform, view_visibility) in spot_light_textures { + // If the texture is invisible, skip it. if !view_visibility.get() { continue; } render_decals.insert_decal( decal_entity, - &texture.image.id(), + [Some(texture.image.id()), None, None, None], global_transform.affine().inverse().into(), 0, ); } +} - for (decal_entity, texture, global_transform, view_visibility) in &point_light_textures { - // If the decal is invisible, skip it. +/// Extracts all textures from point lights from the main world to the render +/// world as clustered decals. +fn extract_point_light_textures( + point_light_textures: &Extract< + Query<( + RenderEntity, + &PointLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + for (decal_entity, texture, global_transform, view_visibility) in point_light_textures { + // If the texture is invisible, skip it. if !view_visibility.get() { continue; } render_decals.insert_decal( decal_entity, - &texture.image.id(), + [Some(texture.image.id()), None, None, None], global_transform.affine().inverse().into(), texture.cubemap_layout as u32, ); } +} - for (decal_entity, texture, global_transform, view_visibility) in &directional_light_textures { - // If the decal is invisible, skip it. +/// Extracts all textures from directional lights from the main world to the +/// render world as clustered decals. +fn extract_directional_light_textures( + directional_light_textures: &Extract< + Query<( + RenderEntity, + &DirectionalLightTexture, + &GlobalTransform, + &ViewVisibility, + )>, + >, + render_decals: &mut RenderClusteredDecals, +) { + for (decal_entity, texture, global_transform, view_visibility) in directional_light_textures { + // If the texture is invisible, skip it. if !view_visibility.get() { continue; } render_decals.insert_decal( decal_entity, - &texture.image.id(), + [Some(texture.image.id()), None, None, None], global_transform.affine().inverse().into(), if texture.tiled { 1 } else { 0 }, ); @@ -376,6 +467,8 @@ impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> { while texture_views.len() < max_view_decals as usize { texture_views.push(&*fallback_image.d2.texture_view); } + } else if texture_views.is_empty() { + texture_views.push(&*fallback_image.d2.texture_view); } Some(RenderViewClusteredDecalBindGroupEntries { @@ -389,12 +482,12 @@ impl<'a> RenderViewClusteredDecalBindGroupEntries<'a> { impl RenderClusteredDecals { /// Returns the index of the given image in the decal texture binding array, /// adding it to the list if necessary. - fn get_or_insert_image(&mut self, image_id: &AssetId) -> u32 { + fn get_or_insert_image(&mut self, image_id: &AssetId) -> i32 { *self .texture_to_binding_index .entry(*image_id) .or_insert_with(|| { - let index = self.binding_index_to_textures.len() as u32; + let index = self.binding_index_to_textures.len() as i32; self.binding_index_to_textures.push(*image_id); index }) diff --git a/crates/bevy_pbr/src/decal/clustered.wgsl b/crates/bevy_pbr/src/decal/clustered.wgsl index 874722a2aba80..a6404ecde0c67 100644 --- a/crates/bevy_pbr/src/decal/clustered.wgsl +++ b/crates/bevy_pbr/src/decal/clustered.wgsl @@ -31,14 +31,31 @@ #import bevy_pbr::clustered_forward #import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges #import bevy_pbr::mesh_view_bindings +#import bevy_pbr::pbr_functions +#import bevy_pbr::pbr_types::PbrInput +#import bevy_pbr::utils::porter_duff_over #import bevy_render::maths +#ifdef MESHLET_MESH_MATERIAL_PASS +#import bevy_pbr::meshlet_visibility_buffer_resolve::VertexOutput +#else ifdef PREPASS_PIPELINE +#import bevy_pbr::prepass_io::VertexOutput +#else +#import bevy_pbr::forward_io::VertexOutput +#endif + // An object that allows stepping through all clustered decals that affect a // single fragment. struct ClusteredDecalIterator { // Public fields follow: - // The index of the decal texture in the binding array. - texture_index: i32, + // The index of the decal base color texture in the binding array. + base_color_texture_index: i32, + // The index of the decal normal map texture in the binding array. + normal_map_texture_index: i32, + // The index of the decal metallic-roughness texture in the binding array. + metallic_roughness_texture_index: i32, + // The index of the decal emissive texture in the binding array. + emissive_texture_index: i32, // The UV coordinates at which to sample that decal texture. uv: vec2, // A custom tag you can use for your own purposes. @@ -71,6 +88,9 @@ fn clustered_decal_iterator_new( clusterable_object_index_ranges: ptr ) -> ClusteredDecalIterator { return ClusteredDecalIterator( + -1, + -1, + -1, -1, vec2(0.0), 0u, @@ -103,8 +123,20 @@ fn clustered_decal_iterator_next(iterator: ptr vec4((*iterator).world_position, 1.0)).xyz; if (all(decal_space_vector >= vec3(-0.5)) && all(decal_space_vector <= vec3(0.5))) { - (*iterator).texture_index = - i32(mesh_view_bindings::clustered_decals.decals[decal_index].image_index); + (*iterator).base_color_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].base_color_texture_index + ); + (*iterator).normal_map_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].normal_map_texture_index + ); + (*iterator).metallic_roughness_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[ + decal_index + ].metallic_roughness_texture_index + ); + (*iterator).emissive_texture_index = i32( + mesh_view_bindings::clustered_decals.decals[decal_index].emissive_texture_index + ); (*iterator).uv = decal_space_vector.xy * vec2(1.0, -1.0) + vec2(0.5); (*iterator).tag = mesh_view_bindings::clustered_decals.decals[decal_index].tag; @@ -135,17 +167,16 @@ fn view_is_orthographic() -> bool { return mesh_view_bindings::view.clip_from_view[3].w == 1.0; } -// Modifies the base color at the given position to account for decals. -// -// Returns the new base color with decals taken into account. If no decals -// overlap the current world position, returns the supplied base color -// unmodified. -fn apply_decal_base_color( - world_position: vec3, - frag_coord: vec2, - initial_base_color: vec4, -) -> vec4 { - var base_color = initial_base_color; +fn apply_decals(pbr_input: ptr) { + let world_position = (*pbr_input).world_position.xyz; + let world_normal = (*pbr_input).world_normal; + let frag_coord = (*pbr_input).frag_coord.xy; + + var base_color = (*pbr_input).material.base_color; + var emissive = (*pbr_input).material.emissive; + var Nt = (*pbr_input).N; + var metallic = (*pbr_input).material.metallic; + var perceptual_roughness = (*pbr_input).material.perceptual_roughness; #ifdef CLUSTERED_DECALS_ARE_USABLE // Fetch the clusterable object index ranges for this world position. @@ -162,22 +193,73 @@ fn apply_decal_base_color( var iterator = clustered_decal_iterator_new(world_position, &clusterable_object_index_ranges); while (clustered_decal_iterator_next(&iterator)) { - // Sample the current decal. - let decal_base_color = textureSampleLevel( - mesh_view_bindings::clustered_decal_textures[iterator.texture_index], - mesh_view_bindings::clustered_decal_sampler, - iterator.uv, - 0.0 - ); - - // Blend with the accumulated fragment. - base_color = vec4( - mix(base_color.rgb, decal_base_color.rgb, decal_base_color.a), - base_color.a + decal_base_color.a - ); + // Apply base color and metallic/roughness. + if (iterator.base_color_texture_index >= 0) { + let decal_base_color = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.base_color_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + + // Apply the metallic (blue channel) and the roughness (green channel) map. + if (iterator.metallic_roughness_texture_index >= 0) { + let metallic_roughness_sampler = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[ + iterator.metallic_roughness_texture_index + ], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + // Use OVER compositing using the base color alpha. + metallic = mix( + metallic * base_color.a, + metallic_roughness_sampler.b, + decal_base_color.a + ); + perceptual_roughness = mix( + perceptual_roughness * base_color.a, + metallic_roughness_sampler.g, + decal_base_color.a + ); + } + + // Apply base color with the standard OVER compositing operator. + base_color = porter_duff_over(base_color, decal_base_color); + } + +#ifdef VERTEX_TANGENTS + if (iterator.normal_map_texture_index >= 0) { + let Nd = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.normal_map_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ).rgb * 2.0 - 1.0; + // This is the *Whiteout* normal map blending operator from [1]. + // + // [1]: https://blog.selfshadow.com/publications/blending-in-detail/ + Nt = vec3(Nt.xy + Nd.xy, Nt.z * Nd.z); + } +#endif // VERTEX_TANGENTS + + // Apply emissive. + if (iterator.emissive_texture_index >= 0) { + let decal_emissive = textureSampleLevel( + mesh_view_bindings::clustered_decal_textures[iterator.emissive_texture_index], + mesh_view_bindings::clustered_decal_sampler, + iterator.uv, + 0.0 + ); + emissive += vec4(decal_emissive.rgb, 0.0); + } } #endif // CLUSTERED_DECALS_ARE_USABLE - return base_color; + (*pbr_input).material.base_color = base_color; + (*pbr_input).material.emissive = emissive; + (*pbr_input).N = normalize(Nt); + (*pbr_input).material.metallic = metallic; + (*pbr_input).material.perceptual_roughness = perceptual_roughness; } - diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 19f87b37946a4..112556ee1eb76 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -176,10 +176,14 @@ struct OrderIndependentTransparencySettings { struct ClusteredDecal { local_from_world: mat4x4, - image_index: i32, + base_color_texture_index: i32, + normal_map_texture_index: i32, + metallic_roughness_texture_index: i32, + emissive_texture_index: i32, tag: u32, pad_a: u32, pad_b: u32, + pad_c: u32, } struct ClusteredDecals { diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index e4d8fe63791d7..ea2b33ee0ac11 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -2,7 +2,7 @@ pbr_types, pbr_functions::alpha_discard, pbr_fragment::pbr_input_from_standard_material, - decal::clustered::apply_decal_base_color, + decal::clustered::apply_decals, } #ifdef PREPASS_PIPELINE @@ -69,11 +69,7 @@ fn fragment( pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color); // clustered decals - pbr_input.material.base_color = apply_decal_base_color( - in.world_position.xyz, - in.position.xy, - pbr_input.material.base_color - ); + apply_decals(&pbr_input); #ifdef PREPASS_PIPELINE // write the gbuffer, lighting pass id, and optionally normal and motion_vector textures diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 8e91aeb9c0361..706e30fb0b201 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -203,3 +203,9 @@ fn dir_to_cube_uv(dir: vec3f) -> CubeUV { // Convert from [-1,1] to [0,1] return CubeUV(uv * 0.5 + 0.5, face); } + +// The Porter-Duff OVER operator on RGBA, correctly computing alpha of the +// result. +fn porter_duff_over(bg: vec4, fg: vec4) -> vec4 { + return vec4(mix(bg.rgb * bg.a, fg.rgb, fg.a), bg.a + fg.a * (1.0 - bg.a)); +} diff --git a/examples/3d/clustered_decal_maps.rs b/examples/3d/clustered_decal_maps.rs new file mode 100644 index 0000000000000..f72aa1df40b99 --- /dev/null +++ b/examples/3d/clustered_decal_maps.rs @@ -0,0 +1,445 @@ +//! Demonstrates the normal map, metallic-roughness map, and emissive features +//! of clustered decals. + +use std::{f32::consts::PI, time::Duration}; + +use bevy::{ + asset::io::web::WebAssetPlugin, + color::palettes::css::{CRIMSON, GOLD}, + image::ImageLoaderSettings, + light::ClusteredDecal, + prelude::*, + render::view::Hdr, +}; +use rand::Rng; + +use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender}; + +#[path = "../helpers/widgets.rs"] +mod widgets; + +/// The demonstration textures that we use. +/// +/// We cache these for efficiency. +#[derive(Resource)] +struct AppTextures { + /// The base color that all our decals have (the Bevy logo). + decal_base_color_texture: Handle, + + /// A normal map that all our decals have. + /// + /// This provides a nice raised embossed look. + decal_normal_map_texture: Handle, + + /// The metallic-roughness map that all our decals have. + /// + /// Metallic is in the blue channel and roughness is in the green channel, + /// like glTF requires. + decal_metallic_roughness_map_texture: Handle, + + /// The emissive texture that can optionally be enabled. + /// + /// This causes the white bird to glow. + decal_emissive_texture: Handle, +} + +impl FromWorld for AppTextures { + fn from_world(world: &mut World) -> Self { + // Load all the decal textures. + let asset_server = world.resource::(); + AppTextures { + decal_base_color_texture: asset_server.load("branding/bevy_bird_dark.png"), + decal_normal_map_texture: asset_server.load_with_settings( + get_web_asset_url("BevyLogo-Normal.png"), + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + ), + decal_metallic_roughness_map_texture: asset_server.load_with_settings( + get_web_asset_url("BevyLogo-MetallicRoughness.png"), + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + ), + decal_emissive_texture: asset_server.load(get_web_asset_url("BevyLogo-Emissive.png")), + } + } +} + +/// A component that we place on our decals to track them for animation +/// purposes. +#[derive(Component)] +struct ExampleDecal { + /// The width and height of the square decal in meters. + size: f32, + /// What state the decal is in (animating in, idling, or animating out). + state: ExampleDecalState, +} + +/// The animation state of a decal. +/// +/// When each [`Timer`] goes off, the decal advances to the next state. +enum ExampleDecalState { + /// The decal has just been spawned and is animating in. + AnimatingIn(Timer), + /// The decal has animated in and is waiting to animate out. + Idling(Timer), + /// The decal is animating out. + /// + /// When this timer expires, the decal is despawned. + AnimatingOut(Timer), +} + +/// All settings that the user can change. +/// +/// This app only has one: whether newly-spawned decals are emissive. +#[derive(Clone, Copy, PartialEq)] +enum AppSetting { + /// True if newly-spawned decals have an emissive channel (i.e. they glow), + /// or false otherwise. + EmissiveDecals(bool), +} + +/// The current values of the settings that the user can change. +/// +/// This app only has one: whether newly-spawned decals are emissive. +#[derive(Default, Resource)] +struct AppStatus { + /// True if newly-spawned decals have an emissive channel (i.e. they glow), + /// or false otherwise. + emissive_decals: bool, +} + +/// Half of the width and height of the plane onto which the decals are +/// projected. +const PLANE_HALF_SIZE: f32 = 2.0; +/// The minimum width and height that a decal may have. +/// +/// The actual size is determined randomly, using this value as a lower bound. +const DECAL_MIN_SIZE: f32 = 0.5; +/// The maximum width and height that a decal may have. +/// +/// The actual size is determined randomly, using this value as an upper bound. +const DECAL_MAX_SIZE: f32 = 1.5; + +/// How long it takes the decal to grow to its full size when animating in. +const DECAL_ANIMATE_IN_DURATION: Duration = Duration::from_millis(300); +/// How long a decal stays in the idle state before starting to animate out. +const DECAL_IDLE_DURATION: Duration = Duration::from_secs(10); +/// How long it takes the decal to shrink down to nothing when animating out. +const DECAL_ANIMATE_OUT_DURATION: Duration = Duration::from_millis(300); + +/// The demo entry point. +fn main() { + App::new() + .add_plugins( + DefaultPlugins + .set(WebAssetPlugin { + silence_startup_warning: true, + }) + .set(WindowPlugin { + primary_window: Some(Window { + title: "Bevy Clustered Decal Maps Example".into(), + ..default() + }), + ..default() + }), + ) + .add_message::>() + .init_resource::() + .init_resource::() + .add_systems(Startup, setup) + .add_systems(Update, draw_gizmos) + .add_systems(Update, spawn_decal) + .add_systems(Update, animate_decals) + .add_systems( + Update, + ( + widgets::handle_ui_interactions::, + update_radio_buttons, + ), + ) + .add_systems( + Update, + handle_emission_type_change.after(widgets::handle_ui_interactions::), + ) + .run(); +} + +/// Spawns all the objects in the scene. +fn setup( + mut commands: Commands, + asset_server: Res, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + spawn_plane_mesh(&mut commands, &asset_server, &mut meshes, &mut materials); + spawn_light(&mut commands); + spawn_camera(&mut commands); + spawn_buttons(&mut commands); +} + +/// Spawns the plane onto which the decals are projected. +fn spawn_plane_mesh( + commands: &mut Commands, + asset_server: &AssetServer, + meshes: &mut Assets, + materials: &mut Assets, +) { + // Create a plane onto which we project decals. + // + // As the plane has a normal map, we must generate tangents for the + // vertices. + let plane_mesh = meshes.add( + Plane3d { + normal: Dir3::NEG_Z, + half_size: Vec2::splat(PLANE_HALF_SIZE), + } + .mesh() + .build() + .with_duplicated_vertices() + .with_computed_flat_normals() + .with_generated_tangents() + .unwrap(), + ); + + // Give the plane some texture. + // + // Note that, as this is a normal map, we must disable sRGB when loading. + let normal_map_texture = asset_server.load_with_settings( + "textures/ScratchedGold-Normal.png", + |settings: &mut ImageLoaderSettings| settings.is_srgb = false, + ); + + // Actually spawn the plane. + commands.spawn(( + Mesh3d(plane_mesh), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::from(CRIMSON), + normal_map_texture: Some(normal_map_texture), + ..StandardMaterial::default() + })), + Transform::IDENTITY, + )); +} + +/// Spawns a light to illuminate the scene. +fn spawn_light(commands: &mut Commands) { + commands.spawn(( + PointLight { + intensity: 10_000_000., + range: 100.0, + ..default() + }, + Transform::from_xyz(8.0, 16.0, -8.0), + )); +} + +/// Spawns a camera. +fn spawn_camera(commands: &mut Commands) { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(2.0, 0.0, -7.0).looking_at(Vec3::ZERO, Vec3::Y), + Hdr, + )); +} + +/// Spawns all the buttons at the bottom of the screen. +fn spawn_buttons(commands: &mut Commands) { + commands.spawn(( + widgets::main_ui_node(), + children![widgets::option_buttons( + "Emissive Decals", + &[ + (AppSetting::EmissiveDecals(true), "On"), + (AppSetting::EmissiveDecals(false), "Off"), + ], + ),], + )); +} + +/// Draws the outlines that show the bounds of the clustered decals. +fn draw_gizmos(mut gizmos: Gizmos, decals: Query<&GlobalTransform, With>) { + for global_transform in &decals { + gizmos.primitive_3d( + &Cuboid { + // Since the clustered decal is a 1×1×1 cube in model space, its + // half-size is half of the scaling part of its transform. + half_size: global_transform.scale() * 0.5, + }, + Isometry3d { + rotation: global_transform.rotation(), + translation: global_transform.translation_vec3a(), + }, + GOLD, + ); + } +} + +/// A system that spawns new decals at fixed intervals. +fn spawn_decal( + mut commands: Commands, + app_status: Res, + app_textures: Res, + time: Res`, found `A` + | this expression has type `&mut Box` + | + = note: expected struct `Box` + found struct `A` + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider dereferencing to access the inner value using the Deref trait + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1461:57 + | +146| let $struct_name { $($field_index: _),* } = &**value; + | +++ + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:31:5 + | +31 | / deconstruct_moving_ptr!({ +32 | | let A { x } = box_a; +33 | | }); + | |______^ expected `A`, found `Box` + | + = note: expected struct `A` + found struct `Box` + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:35:5 + | +35 | / deconstruct_moving_ptr!({ +36 | | let A { x } = mut_a; +37 | | }); + | |______^ expected `A`, found `&mut A` + | + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:39:5 + | +39 | / deconstruct_moving_ptr!({ +40 | | let tuple { 0: _ } = box_t1; +41 | | }); + | | ^ + | | | + | |______expected `Box<(usize,)>`, found `(_,)` + | expected due to the type of this binding + | + = note: expected struct `Box<(usize,)>` + found tuple `(_,)` + = note: for more on the distinction between the stack and the heap, read https://doc.rust-lang.org/book/ch15-01-box.html, https://doc.rust-lang.org/rust-by-example/std/box.html, and https://doc.rust-lang.org/std/boxed/index.html + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) +help: store this in the heap by calling `Box::new` + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1420:22 + | +142| *value = Box::new(($(unreachable($field_index),)*)); + | +++++++++ + +help: consider dereferencing here to assign to the mutably borrowed value + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1420:13 + | +142| **value = ($(unreachable($field_index),)*); + | + + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:43:5 + | +43 | / deconstruct_moving_ptr!({ +44 | | let tuple { 0: _ } = mut_t1; +45 | | }); + | | ^ + | | | + | |______expected `&mut (usize,)`, found `(_,)` + | expected due to the type of this binding + | + = note: expected mutable reference `&mut (usize,)` + found tuple `(_,)` + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider dereferencing here to assign to the mutably borrowed value + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1420:13 + | +142| **value = ($(unreachable($field_index),)*); + | + + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:47:5 + | +47 | / deconstruct_moving_ptr!({ +48 | | let tuple { 0: _, 1: _ } = box_t2; +49 | | }); + | | ^ + | | | + | |______expected `Box<(usize, usize)>`, found `(_, _)` + | expected due to the type of this binding + | + = note: expected struct `Box<(usize, usize)>` + found tuple `(_, _)` + = note: for more on the distinction between the stack and the heap, read https://doc.rust-lang.org/book/ch15-01-box.html, https://doc.rust-lang.org/rust-by-example/std/box.html, and https://doc.rust-lang.org/std/boxed/index.html + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) +help: store this in the heap by calling `Box::new` + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1420:22 + | +142| *value = Box::new(($(unreachable($field_index),)*)); + | +++++++++ + +help: consider dereferencing here to assign to the mutably borrowed value + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1420:13 + | +142| **value = ($(unreachable($field_index),)*); + | + + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:51:5 + | +51 | / deconstruct_moving_ptr!({ +52 | | let tuple { 0: _, 1: _ } = mut_t2; +53 | | }); + | | ^ + | | | + | |______expected `&mut (usize, usize)`, found `(_, _)` + | expected due to the type of this binding + | + = note: expected mutable reference `&mut (usize, usize)` + found tuple `(_, _)` + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider dereferencing here to assign to the mutably borrowed value + --> $BEVY_ROOT/bevy_ptr/src/lib.rs:1420:13 + | +142| **value = ($(unreachable($field_index),)*); + | + + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:77:23 + | +75 | / deconstruct_moving_ptr!({ +76 | | +77 | | let A { x } = a; + | | ^ expected `MovingPtr<'_, _, _>`, found `&mut MovingPtr<'_, A>` +78 | | }); + | |______- expected due to this + | + = note: expected struct `MovingPtr<'_, _, _>` + found mutable reference `&mut MovingPtr<'_, A, Aligned>` + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:81:30 + | +79 | / deconstruct_moving_ptr!({ +80 | | +81 | | let tuple { 0: _ } = t1; + | | ^^ expected `MovingPtr<'_, _, _>`, found `&mut MovingPtr<'_, (usize,)>` +82 | | }); + | |______- expected due to this + | + = note: expected struct `MovingPtr<'_, _, _>` + found mutable reference `&mut MovingPtr<'_, (usize,), Aligned>` + +error[E0308]: mismatched types + --> tests/ui/deconstruct_moving_ptr.rs:85:36 + | +83 | / deconstruct_moving_ptr!({ +84 | | +85 | | let tuple { 0: _, 1: _ } = t2; + | | ^^ expected `MovingPtr<'_, _, _>`, found `&mut MovingPtr<'_, (usize, usize)>` +86 | | }); + | |______- expected due to this + | + = note: expected struct `MovingPtr<'_, _, _>` + found mutable reference `&mut MovingPtr<'_, (usize, usize), Aligned>` + +error[E0499]: cannot borrow value as mutable more than once at a time + --> tests/ui/deconstruct_moving_ptr.rs:59:5 + | +59 | / deconstruct_moving_ptr!({ +60 | | let tuple { 0: _, 0: _ } = t; +61 | | }); + | | ^ + | | | + | |______value was mutably borrowed here in the previous iteration of the loop + | first borrow later used here + | + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0793]: reference to packed field is unaligned + --> tests/ui/deconstruct_moving_ptr.rs:67:5 + | +67 | / deconstruct_moving_ptr!({ +68 | | let B { x } = b; +69 | | }); + | |______^ + | + = note: packed structs are only aligned by one byte, and many modern architectures penalize unaligned field accesses + = note: creating a misaligned reference is undefined behavior (even if that reference is never dereferenced) + = help: copy the field contents to a local variable, or replace the reference with a raw pointer and use `read_unaligned`/`write_unaligned` (loads and stores via `*p` must be properly aligned even when using raw pointers) + = note: this error originates in the macro `deconstruct_moving_ptr` (in Nightly builds, run with -Z macro-backtrace for more info) + +warning: unused variable: `x` + --> tests/ui/deconstruct_moving_ptr.rs:68:17 + | +68 | let B { x } = b; + | ^ help: if this is intentional, prefix it with an underscore: `_x` + | + = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default + +error: aborting due to 13 previous errors; 1 warning emitted + +Some errors have detailed explanations: E0025, E0308, E0499, E0793. +For more information about an error, try `rustc --explain E0025`. diff --git a/crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.stderr index ba14761f58e8f..65c3ed45a91b2 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/query_exact_sized_iterator_safety.stderr @@ -1,7 +1,7 @@ error[E0277]: `bevy_ecs::query::Changed` is not a valid `Query` filter based on archetype information --> tests/ui/query_exact_sized_iterator_safety.rs:7:28 | -7 | is_exact_size_iterator(query.iter()); + 7 | is_exact_size_iterator(query.iter()); | ---------------------- ^^^^^^^^^^^^ invalid `Query` filter | | | required by a bound introduced by this call diff --git a/crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.stderr index cb40b8f11ccb5..9c382e6e403f7 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_combinations_mut_iterator_safety.stderr @@ -1,7 +1,7 @@ error[E0277]: the trait bound `&mut A: ReadOnlyQueryData` is not satisfied --> tests/ui/query_iter_combinations_mut_iterator_safety.rs:9:17 | -9 | is_iterator(iter) + 9 | is_iterator(iter) | ----------- ^^^^ the trait `ReadOnlyQueryData` is not implemented for `&mut A` | | | required by a bound introduced by this call @@ -15,7 +15,7 @@ error[E0277]: the trait bound `&mut A: ReadOnlyQueryData` is not satisfied (F0, F1, F2) (F0, F1, F2, F3) (F0, F1, F2, F3, F4) - and 36 others + and 37 others = note: `ReadOnlyQueryData` is implemented for `&A`, but not for `&mut A` = note: required for `QueryCombinationIter<'_, '_, &mut A, (), _>` to implement `Iterator` note: required by a bound in `is_iterator` diff --git a/crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.stderr index 24aafb1df49fc..a00fc7c321c5a 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/query_iter_many_mut_iterator_safety.stderr @@ -1,7 +1,7 @@ error[E0277]: the trait bound `&mut A: ReadOnlyQueryData` is not satisfied --> tests/ui/query_iter_many_mut_iterator_safety.rs:9:17 | -9 | is_iterator(iter) + 9 | is_iterator(iter) | ----------- ^^^^ the trait `ReadOnlyQueryData` is not implemented for `&mut A` | | | required by a bound introduced by this call @@ -15,7 +15,7 @@ error[E0277]: the trait bound `&mut A: ReadOnlyQueryData` is not satisfied (F0, F1, F2) (F0, F1, F2, F3) (F0, F1, F2, F3, F4) - and 36 others + and 37 others = note: `ReadOnlyQueryData` is implemented for `&A`, but not for `&mut A` = note: required for `QueryManyIter<'_, '_, &mut A, (), std::array::IntoIter>` to implement `Iterator` note: required by a bound in `is_iterator` diff --git a/crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.stderr index c39840127ad45..96ad14019be8c 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/query_lifetime_safety.stderr @@ -23,9 +23,9 @@ error[E0502]: cannot borrow `query` as immutable because it is also borrowed as error[E0502]: cannot borrow `query` as mutable because it is also borrowed as immutable --> tests/ui/query_lifetime_safety.rs:31:39 | -30 | let data: &Foo = query.single(); +30 | let data: &Foo = query.single().unwrap(); | ----- immutable borrow occurs here -31 | let mut data2: Mut = query.single_mut(); +31 | let mut data2: Mut = query.single_mut().unwrap(); | ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here 32 | 33 | assert_eq!(data, &mut *data2); // oops UB @@ -34,9 +34,9 @@ error[E0502]: cannot borrow `query` as mutable because it is also borrowed as im error[E0502]: cannot borrow `query` as immutable because it is also borrowed as mutable --> tests/ui/query_lifetime_safety.rs:38:30 | -37 | let mut data2: Mut = query.single_mut(); +37 | let mut data2: Mut = query.single_mut().unwrap(); | ----- mutable borrow occurs here -38 | let data: &Foo = query.single(); +38 | let data: &Foo = query.single().unwrap(); | ^^^^^ immutable borrow occurs here 39 | 40 | assert_eq!(data, &mut *data2); // oops UB @@ -48,7 +48,7 @@ error[E0502]: cannot borrow `query` as mutable because it is also borrowed as im 44 | let data: &Foo = query.single().unwrap(); | ----- immutable borrow occurs here 45 | let mut data2: Mut = query.single_mut().unwrap(); - | ^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here + | ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here 46 | 47 | assert_eq!(data, &mut *data2); // oops UB | ----------------------------- immutable borrow later used here diff --git a/crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.stderr index 30cbc3c14221b..8ba81f0954499 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/query_to_readonly.stderr @@ -23,7 +23,7 @@ error[E0502]: cannot borrow `query` as mutable because it is also borrowed as im error[E0502]: cannot borrow `query` as immutable because it is also borrowed as mutable --> tests/ui/query_to_readonly.rs:41:30 | -38 | let mut mut_foo = query.single_mut(); +38 | let mut mut_foo = query.single_mut().unwrap(); | ----- mutable borrow occurs here ... 41 | let readonly_query = query.as_readonly(); @@ -38,11 +38,11 @@ error[E0502]: cannot borrow `query` as mutable because it is also borrowed as im 54 | let readonly_query = query.as_readonly(); | ----- immutable borrow occurs here ... -58 | let mut mut_foo = query.single_mut(); +58 | let mut mut_foo = query.single_mut().unwrap(); | ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here ... 61 | println!("{ref_foo:?}"); - | ----------- immutable borrow later used here + | ------- immutable borrow later used here error: aborting due to 4 previous errors diff --git a/crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.stderr index d0234b197e595..edd87da242c11 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/query_transmute_safety.stderr @@ -17,7 +17,7 @@ error[E0499]: cannot borrow `lens` as mutable more than once at a time 34 | let mut query_b = lens.query(); | ^^^^ second mutable borrow occurs here ... -37 | let a = query_a.single_mut(); +37 | let a = query_a.single_mut().unwrap(); | ------- first borrow later used here error: aborting due to 2 previous errors diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.stderr index 2f767d960cb2f..8d0ad83216136 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_param_derive_readonly.stderr @@ -1,7 +1,7 @@ error[E0277]: the trait bound `&'static mut Foo: ReadOnlyQueryData` is not satisfied --> tests/ui/system_param_derive_readonly.rs:16:11 | -16 | state.get(&world); + 16 | state.get(&world); | ^^^ the trait `ReadOnlyQueryData` is not implemented for `&'static mut Foo` | = help: the following other types implement trait `ReadOnlyQueryData`: @@ -13,18 +13,18 @@ error[E0277]: the trait bound `&'static mut Foo: ReadOnlyQueryData` is not satis (F0, F1, F2) (F0, F1, F2, F3) (F0, F1, F2, F3, F4) - and 36 others + and 37 others = note: `ReadOnlyQueryData` is implemented for `&'static Foo`, but not for `&'static mut Foo` = note: required for `bevy_ecs::system::Query<'_, '_, &'static mut Foo>` to implement `ReadOnlySystemParam` = note: 1 redundant requirement hidden = note: required for `Mutable<'_, '_>` to implement `ReadOnlySystemParam` note: required by a bound in `SystemState::::get` - --> $BEVY_ROOT/bevy_ecs/src/system/function_system.rs:487:16 + --> $BEVY_ROOT/bevy_ecs/src/system/function_system.rs:355:16 | -485 | pub fn get<'w, 's>(&'s mut self, world: &'w World) -> SystemParamItem<'w, 's, Param> +353 | pub fn get<'w, 's>(&'s mut self, world: &'w World) -> SystemParamItem<'w, 's, Param> | --- required by a bound in this associated function -486 | where -487 | Param: ReadOnlySystemParam, +354 | where +355 | Param: ReadOnlySystemParam, | ^^^^^^^^^^^^^^^^^^^ required by this bound in `SystemState::::get` error: aborting due to 1 previous error diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.stderr index cd003db5125fe..be8418424288a 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_lifetime_safety.stderr @@ -1,11 +1,11 @@ error[E0499]: cannot borrow `query` as mutable more than once at a time --> tests/ui/system_query_get_lifetime_safety.rs:8:14 | -7 | let a1 = query.get_mut(e).unwrap(); + 7 | let a1 = query.get_mut(e).unwrap(); | ----- first mutable borrow occurs here -8 | let a2 = query.get_mut(e).unwrap(); + 8 | let a2 = query.get_mut(e).unwrap(); | ^^^^^ second mutable borrow occurs here -9 | + 9 | 10 | println!("{} {}", a1.0, a2.0); | -- first borrow later used here diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.stderr index 91e8b81509e05..310a88f0da935 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_lifetime_safety.stderr @@ -1,11 +1,11 @@ error[E0502]: cannot borrow `query` as mutable because it is also borrowed as immutable --> tests/ui/system_query_get_many_lifetime_safety.rs:8:14 | -7 | let a1 = query.get_many([e, e]).unwrap(); + 7 | let a1 = query.get_many([e, e]).unwrap(); | ----- immutable borrow occurs here -8 | let a2 = query.get_mut(e).unwrap(); + 8 | let a2 = query.get_mut(e).unwrap(); | ^^^^^^^^^^^^^^^^ mutable borrow occurs here -9 | + 9 | 10 | println!("{} {}", a1[0].0, a2.0); | ----- immutable borrow later used here diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.stderr index 567a1da1a683e..fb770e42b2a9c 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_get_many_mut_lifetime_safety.stderr @@ -1,11 +1,11 @@ error[E0499]: cannot borrow `query` as mutable more than once at a time --> tests/ui/system_query_get_many_mut_lifetime_safety.rs:8:14 | -7 | let a1 = query.get_many_mut([e, e]).unwrap(); + 7 | let a1 = query.get_many_mut([e, e]).unwrap(); | ----- first mutable borrow occurs here -8 | let a2 = query.get_mut(e).unwrap(); + 8 | let a2 = query.get_mut(e).unwrap(); | ^^^^^ second mutable borrow occurs here -9 | + 9 | 10 | println!("{} {}", a1[0].0, a2.0); | ----- first borrow later used here diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.stderr index bcae3940a4811..309bd73fd7c79 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_lifetime_safety.stderr @@ -1,7 +1,7 @@ error[E0499]: cannot borrow `query` as mutable more than once at a time --> tests/ui/system_query_iter_lifetime_safety.rs:10:21 | -7 | let mut iter = query.iter_mut(); + 7 | let mut iter = query.iter_mut(); | ----- first mutable borrow occurs here ... 10 | let mut iter2 = query.iter_mut(); diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_sort_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_sort_lifetime_safety.stderr index 4565ece326b29..d3966c0fd158f 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_sort_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_iter_sort_lifetime_safety.stderr @@ -1,7 +1,7 @@ error[E0521]: borrowed data escapes outside of closure --> tests/ui/system_query_iter_sort_lifetime_safety.rs:12:9 | -9 | let mut stored: Option<&A> = None; + 9 | let mut stored: Option<&A> = None; | ---------- `stored` declared here, outside of the closure body 10 | let mut sorted = iter.sort_by::<&A>(|left, _right| { | ---- `left` is a reference that is only valid in the closure body diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.stderr index be0598f1eb767..1aaceb1721ad8 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_get_lifetime_safety.stderr @@ -1,7 +1,7 @@ error[E0499]: cannot borrow `queries` as mutable more than once at a time --> tests/ui/system_query_set_get_lifetime_safety.rs:10:14 | -7 | let mut q2 = queries.p0(); + 7 | let mut q2 = queries.p0(); | ------- first mutable borrow occurs here ... 10 | let q1 = queries.p1(); diff --git a/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.stderr b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.stderr index 8d1a50bd3f8cc..8aebf6c7b6c4d 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/system_query_set_iter_lifetime_safety.stderr @@ -1,7 +1,7 @@ error[E0499]: cannot borrow `queries` as mutable more than once at a time --> tests/ui/system_query_set_iter_lifetime_safety.rs:11:14 | -7 | let mut q2 = queries.p0(); + 7 | let mut q2 = queries.p0(); | ------- first mutable borrow occurs here ... 11 | let q1 = queries.p1(); diff --git a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr index ec71c112a6a05..2ece1b014ebd1 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr +++ b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.stderr @@ -19,20 +19,25 @@ error[E0277]: the trait bound `&'static mut Foo: ReadOnlyQueryData` is not satis (F0, F1, F2) (F0, F1, F2, F3) (F0, F1, F2, F3, F4) - and 39 others + and 40 others note: required by a bound in `_::assert_readonly` --> tests/ui/world_query_derive.rs:7:10 | -7 | #[derive(QueryData)] + 7 | #[derive(QueryData)] | ^^^^^^^^^ required by this bound in `assert_readonly` = note: this error originates in the derive macro `QueryData` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `MutableMarked: ReadOnlyQueryData` is not satisfied - --> tests/ui/world_query_derive.rs:43:8 + --> tests/ui/world_query_derive.rs:29:8 | -43 | a: MutableMarked, - | ^^^^^^^^^^^^^ the trait `ReadOnlyQueryData` is not implemented for `MutableMarked` +29 | a: MutableMarked, + | ^^^^^^^^^^^^^ unsatisfied trait bound | +help: the trait `ReadOnlyQueryData` is not implemented for `MutableMarked` + --> tests/ui/world_query_derive.rs:22:1 + | +22 | struct MutableMarked { + | ^^^^^^^^^^^^^^^^^^^^ = help: the following other types implement trait `ReadOnlyQueryData`: &Archetype &T @@ -42,14 +47,14 @@ error[E0277]: the trait bound `MutableMarked: ReadOnlyQueryData` is not satisfie (F0, F1, F2) (F0, F1, F2, F3) (F0, F1, F2, F3, F4) - and 39 others + and 40 others note: required by a bound in `_::assert_readonly` - --> tests/ui/world_query_derive.rs:40:10 + --> tests/ui/world_query_derive.rs:26:10 | -40 | #[derive(QueryData)] +26 | #[derive(QueryData)] | ^^^^^^^^^ required by this bound in `assert_readonly` = note: this error originates in the derive macro `QueryData` (in Nightly builds, run with -Z macro-backtrace for more info) -error: aborting due to 5 previous errors +error: aborting due to 3 previous errors For more information about this error, try `rustc --explain E0277`. diff --git a/crates/bevy_ecs/src/system/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index f87182ab1f13c..89bd1b0d8bcfe 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -100,6 +100,7 @@ macro_rules! impl_exclusive_system_param_tuple { unused_variables, reason = "Zero-length tuples won't use any of the parameters." )] + #[allow(clippy::unused_unit, reason = "Zero length tuple is unit.")] $(#[$meta])* impl<$($param: ExclusiveSystemParam),*> ExclusiveSystemParam for ($($param,)*) { type State = ($($param::State,)*); @@ -107,7 +108,7 @@ macro_rules! impl_exclusive_system_param_tuple { #[inline] fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - (($($param::init(world, system_meta),)*)) + ($($param::init(world, system_meta),)*) } #[inline] diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 32b4044e6f25b..de15c850ccdb9 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -2145,6 +2145,7 @@ macro_rules! impl_system_param_tuple { unused_variables, reason = "Zero-length tuples won't use some of the parameters." )] + #[allow(clippy::unused_unit, reason = "Zero length tuple is unit.")] $(#[$meta])* // SAFETY: implementers of each `SystemParam` in the tuple have validated their impls unsafe impl<$($param: SystemParam),*> SystemParam for ($($param,)*) { @@ -2153,7 +2154,7 @@ macro_rules! impl_system_param_tuple { #[inline] fn init_state(world: &mut World) -> Self::State { - (($($param::init_state(world),)*)) + ($($param::init_state(world),)*) } fn init_access(state: &Self::State, _system_meta: &mut SystemMeta, _component_access_set: &mut FilteredAccessSet, _world: &mut World) { diff --git a/crates/bevy_reflect/src/path/parse.rs b/crates/bevy_reflect/src/path/parse.rs index be5856834a9c3..1a8a0c55f8654 100644 --- a/crates/bevy_reflect/src/path/parse.rs +++ b/crates/bevy_reflect/src/path/parse.rs @@ -59,16 +59,16 @@ impl<'a> PathParser<'a> { // If we do not find a subsequent token, we are at the end of the parse string. let ident_len = to_parse.iter().position(|t| Token::SYMBOLS.contains(t)); let (ident, remaining) = to_parse.split_at(ident_len.unwrap_or(to_parse.len())); + #[expect( + unsafe_code, + reason = "We have fulfilled the Safety requirements for `from_utf8_unchecked`." + )] // SAFETY: This relies on `self.remaining` always remaining valid UTF8: // - self.remaining is a slice derived from self.path (valid &str) // - The slice's end is either the same as the valid &str or // the last byte before an ASCII utf-8 character (ie: it is a char // boundary). // - The slice always starts after a symbol ie: an ASCII character's boundary. - #[expect( - unsafe_code, - reason = "We have fulfilled the Safety requirements for `from_utf8_unchecked`." - )] let ident = unsafe { from_utf8_unchecked(ident) }; self.remaining = remaining; diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 5c359dab5b2e1..4ee7f336aefc2 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -1690,8 +1690,6 @@ mod tests { #[test] fn insert_reflect_only_component() { - use bevy_ecs::prelude::Component; - use bevy_reflect::Reflect; #[derive(Reflect, Component)] #[reflect(Component)] struct Player { diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index ec226c760b96b..2bd360c51bee6 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -323,11 +323,7 @@ impl FrameData { ) -> &mut SpanRecord { let thread_id = thread::current().id(); - let parent = self - .open_spans - .iter() - .filter(|v| v.thread_id == thread_id) - .next_back(); + let parent = self.open_spans.iter().rfind(|v| v.thread_id == thread_id); let path_range = match &parent { Some(parent) if parent.path_range.end == self.path_components.len() => { @@ -363,8 +359,7 @@ impl FrameData { let iter = self.open_spans.iter(); let (index, _) = iter .enumerate() - .filter(|(_, v)| v.thread_id == thread_id) - .next_back() + .rfind(|(_, v)| v.thread_id == thread_id) .unwrap(); let span = self.open_spans.swap_remove(index); diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index 62038b37ed90f..34366b1b3ce25 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -881,12 +881,12 @@ mod test { // cannot happen let mut a = unsafe { child_entity.get_mut_assume_mutable::().unwrap() }; - // SAFETY: ChildOf is not mutable but this is for a test to produce a scenario that - // cannot happen #[expect( unsafe_code, reason = "ChildOf is not mutable but this is for a test to produce a scenario that cannot happen" )] + // SAFETY: ChildOf is not mutable but this is for a test to produce a scenario that + // cannot happen let mut b = unsafe { grandchild_entity .get_mut_assume_mutable::() diff --git a/tools/export-content/src/app.rs b/tools/export-content/src/app.rs index 7fa2be0d1a494..f734375ccb45a 100644 --- a/tools/export-content/src/app.rs +++ b/tools/export-content/src/app.rs @@ -1,3 +1,8 @@ +#![expect( + unused_assignments, + reason = "Warnings from inside miette due to a rustc bug: https://github.com/rust-lang/rust/issues/147648" +)] + use std::{env, fs, io::Write, path::PathBuf}; use miette::{diagnostic, Context, Diagnostic, IntoDiagnostic, NamedSource, Result}; From 94914053ebf0dc7119f01e3ecc3477d0bd996f6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B8holm?= Date: Fri, 12 Dec 2025 19:47:13 +0100 Subject: [PATCH 088/133] Fix incorrect blake3 version bound (#22091) # Objective The PR #21925 introduced some code using the constant `blake3::CHUNK_LEN`, but that wasn't public before version 1.8, and bevy_asset currently depends on version 1.5. This breaks users with an older version in their lock file. ## Solution Bump blake3 to 1.8 --- crates/bevy_asset/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index d20e9c0974ee7..fb19e7e841d5a 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -58,7 +58,7 @@ futures-util = { version = "0.3", default-features = false, features = [ ] } futures-io = { version = "0.3", default-features = false } futures-lite = { version = "2.0.1", default-features = false } -blake3 = { version = "1.5", default-features = false } +blake3 = { version = "1.8", default-features = false } ron = { version = "0.12", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } From 53aea52328bf79b5becf95a2ab66883b1d7c8b69 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 12 Dec 2025 20:16:33 +0000 Subject: [PATCH 089/133] text2d change detection fix (#22098) # Objective Fix performance regression caused by #22051 breaking text2d's change detection. Fixes #22099. ## Solution Set the text entity's `ComputedTextBlock::needs_rerender` flag to false at the start of `update_text_buffer` and `update_text_layout_info`. ## Testing ``` cargo run --example many_text2d --release ``` should run about 400x main's FPS --- crates/bevy_text/src/pipeline.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index d9e95e7e598d0..88cbd8ba32cef 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -104,6 +104,8 @@ impl TextPipeline { computed: &mut ComputedTextBlock, font_system: &mut CosmicFontSystem, ) -> Result<(), TextError> { + computed.needs_rerender = false; + let font_system = &mut font_system.0; // Collect span information into a vec. This is necessary because font loading requires mut access @@ -305,6 +307,8 @@ impl TextPipeline { bounds: TextBounds, justify: Justify, ) -> Result<(), TextError> { + computed.needs_rerender = false; + layout_info.glyphs.clear(); layout_info.run_geometry.clear(); layout_info.size = Default::default(); From 5eba4eb98c9f3bc0f7634ac169ae7cea23803847 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Fri, 12 Dec 2025 15:49:38 -0800 Subject: [PATCH 090/133] Fix wrong termination for asset hash reads. (#22100) # Objective - #21925 changed the hash computation to read the file piece by piece. Unfortunately, the termination condition is wrong for this loop. We should only exit once the reader has no more bytes (`bytes_read == 0`), otherwise, the reader might partially fill the buffer, and that may be interpretted as an EOF. - I originally did this check, but something was failing due to never terminating the read. Unclear what happened, maybe I got mixed up! ## Solution - Check if there are no bytes left rather than if the buffer is partially filled. ## Testing - Ran the asset tests with --all-features - Ran the `asset_processor` example and it seems to work! --- crates/bevy_asset/src/meta.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index b6b41d36833cb..f78273d5bd672 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -254,7 +254,7 @@ pub(crate) async fn get_asset_hash( loop { let bytes_read = asset_reader.read(&mut buffer).await?; hasher.update(&buffer[..bytes_read]); - if bytes_read < buffer.len() { + if bytes_read == 0 { // This means we've reached EOF, so we're done consuming asset bytes. break; } From 9c589768c7c25167e011937b3d018a0f53db3724 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sat, 13 Dec 2025 07:53:59 +0800 Subject: [PATCH 091/133] Revert "Support non-sRGB image formats for `RenderTarget::Image`" (#22101) Reverts bevyengine/bevy#22031 Rationale see https://github.com/bevyengine/bevy/pull/22090 --- crates/bevy_render/src/camera.rs | 6 +++--- .../bevy_render/src/texture/manual_texture_view.rs | 13 ++++++++++--- .../bevy_render/src/texture/texture_attachment.rs | 6 ++++-- crates/bevy_render/src/view/mod.rs | 13 ++++++++++--- crates/bevy_render/src/view/window/screenshot.rs | 10 +++++----- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 5c9cf002c2c07..84988a2fc6534 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -213,9 +213,9 @@ impl NormalizedRenderTargetExt for NormalizedRenderTarget { NormalizedRenderTarget::Image(image_target) => images .get(&image_target.handle) .map(|image| image.texture_format), - NormalizedRenderTarget::TextureView(id) => manual_texture_views - .get(id) - .map(|view| view.texture_view.texture().format()), + NormalizedRenderTarget::TextureView(id) => { + manual_texture_views.get(id).map(|tex| tex.format) + } NormalizedRenderTarget::None { .. } => None, } } diff --git a/crates/bevy_render/src/texture/manual_texture_view.rs b/crates/bevy_render/src/texture/manual_texture_view.rs index b03943d3c2750..b9faa84ab78f6 100644 --- a/crates/bevy_render/src/texture/manual_texture_view.rs +++ b/crates/bevy_render/src/texture/manual_texture_view.rs @@ -1,8 +1,10 @@ use bevy_camera::ManualTextureViewHandle; use bevy_ecs::{prelude::Component, resource::Resource}; +use bevy_image::BevyDefault; use bevy_math::UVec2; use bevy_platform::collections::HashMap; use bevy_render_macros::ExtractResource; +use wgpu::TextureFormat; use crate::render_resource::TextureView; @@ -11,11 +13,16 @@ use crate::render_resource::TextureView; pub struct ManualTextureView { pub texture_view: TextureView, pub size: UVec2, + pub format: TextureFormat, } impl ManualTextureView { - pub fn new(texture_view: TextureView, size: UVec2) -> Self { - Self { texture_view, size } + pub fn with_default_format(texture_view: TextureView, size: UVec2) -> Self { + Self { + texture_view, + size, + format: TextureFormat::bevy_default(), + } } } @@ -29,7 +36,7 @@ impl ManualTextureView { /// # world.insert_resource(ManualTextureViews::default()); /// # let texture_view = todo!(); /// let manual_views = world.resource_mut::(); -/// let manual_view = ManualTextureView::new(texture_view, UVec2::new(1024, 1024)); +/// let manual_view = ManualTextureView::with_default_format(texture_view, UVec2::new(1024, 1024)); /// /// // Choose an unused handle value; it's likely only you are inserting manual views. /// const MANUAL_VIEW_HANDLE: ManualTextureViewHandle = ManualTextureViewHandle::new(42); diff --git a/crates/bevy_render/src/texture/texture_attachment.rs b/crates/bevy_render/src/texture/texture_attachment.rs index 26588212683c1..269c2f1422820 100644 --- a/crates/bevy_render/src/texture/texture_attachment.rs +++ b/crates/bevy_render/src/texture/texture_attachment.rs @@ -1,5 +1,5 @@ use super::CachedTexture; -use crate::render_resource::TextureView; +use crate::render_resource::{TextureFormat, TextureView}; use alloc::sync::Arc; use bevy_color::LinearRgba; use core::sync::atomic::{AtomicBool, Ordering}; @@ -127,13 +127,15 @@ impl DepthAttachment { #[derive(Clone)] pub struct OutputColorAttachment { pub view: TextureView, + pub format: TextureFormat, is_first_call: Arc, } impl OutputColorAttachment { - pub fn new(view: TextureView) -> Self { + pub fn new(view: TextureView, format: TextureFormat) -> Self { Self { view, + format, is_first_call: Arc::new(AtomicBool::new(true)), } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index cc39266a6ddec..e9898c9deaddf 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -848,7 +848,7 @@ impl ViewTarget { /// The format of the final texture this view will render to #[inline] pub fn out_texture_format(&self) -> TextureFormat { - self.out_texture.view.texture().format() + self.out_texture.format } /// This will start a new "post process write", which assumes that the caller @@ -1024,7 +1024,10 @@ pub fn prepare_view_attachments( let Some(attachment) = target .get_texture_view(&windows, &images, &manual_texture_views) .cloned() - .map(OutputColorAttachment::new) + .zip(target.get_texture_format(&windows, &images, &manual_texture_views)) + .map(|(view, format)| { + OutputColorAttachment::new(view.clone(), format.add_srgb_suffix()) + }) else { continue; }; @@ -1093,7 +1096,11 @@ pub fn prepare_view_targets( dimension: TextureDimension::D2, format: main_texture_format, usage: texture_usage.0, - view_formats: &[], + view_formats: match main_texture_format { + TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb], + TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb], + _ => &[], + }, }; let a = texture_cache.get( &render_device, diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 4c5f60ac34c1c..4aba6c6cee791 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -295,7 +295,7 @@ fn prepare_screenshots( prepared.insert(*entity, state); view_target_attachments.insert( target.clone(), - OutputColorAttachment::new(texture_view.clone()), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), ); } NormalizedRenderTarget::Image(image) => { @@ -315,7 +315,7 @@ fn prepare_screenshots( prepared.insert(*entity, state); view_target_attachments.insert( target.clone(), - OutputColorAttachment::new(texture_view.clone()), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), ); } NormalizedRenderTarget::TextureView(texture_view) => { @@ -326,7 +326,7 @@ fn prepare_screenshots( ); continue; }; - let format = manual_texture_view.texture_view.texture().format(); + let format = manual_texture_view.format; let size = manual_texture_view.size.to_extents(); let (texture_view, state) = prepare_screenshot_state( size, @@ -339,7 +339,7 @@ fn prepare_screenshots( prepared.insert(*entity, state); view_target_attachments.insert( target.clone(), - OutputColorAttachment::new(texture_view.clone()), + OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), ); } NormalizedRenderTarget::None { .. } => { @@ -550,7 +550,7 @@ pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEnc }; let width = texture_view.size.x; let height = texture_view.size.y; - let texture_format = texture_view.texture_view.texture().format(); + let texture_format = texture_view.format; let texture_view = texture_view.texture_view.deref(); render_screenshot( encoder, From f2592b0a72ed238161208a7ce01587aff2ec30e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Sun, 14 Dec 2025 01:15:41 +0100 Subject: [PATCH 092/133] make example clustered_decal_maps deterministic (#22109) # Objective - don't use random random ## Solution - use seeded random --- examples/3d/clustered_decal_maps.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/3d/clustered_decal_maps.rs b/examples/3d/clustered_decal_maps.rs index f72aa1df40b99..0f88747fd1fd9 100644 --- a/examples/3d/clustered_decal_maps.rs +++ b/examples/3d/clustered_decal_maps.rs @@ -11,7 +11,8 @@ use bevy::{ prelude::*, render::view::Hdr, }; -use rand::Rng; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; use crate::widgets::{RadioButton, RadioButtonText, WidgetClickEvent, WidgetClickSender}; @@ -159,9 +160,13 @@ fn main() { Update, handle_emission_type_change.after(widgets::handle_ui_interactions::), ) + .insert_resource(SeededRng(ChaCha8Rng::seed_from_u64(19878367467712))) .run(); } +#[derive(Resource)] +struct SeededRng(ChaCha8Rng); + /// Spawns all the objects in the scene. fn setup( mut commands: Commands, @@ -279,6 +284,7 @@ fn spawn_decal( app_textures: Res, time: Res()); + } + }; } } else { extracted_assets.push((id, asset.clone())); diff --git a/crates/bevy_render/src/storage.rs b/crates/bevy_render/src/storage.rs index d7c208f001bf3..0362add4ed959 100644 --- a/crates/bevy_render/src/storage.rs +++ b/crates/bevy_render/src/storage.rs @@ -1,5 +1,5 @@ use crate::{ - render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, + render_asset::{AssetExtractionError, PrepareAssetError, RenderAsset, RenderAssetPlugin}, render_resource::{Buffer, BufferUsages}, renderer::RenderDevice, }; @@ -101,6 +101,7 @@ where /// A storage buffer that is prepared as a [`RenderAsset`] and uploaded to the GPU. pub struct GpuShaderStorageBuffer { pub buffer: Buffer, + pub had_data: bool, } impl RenderAsset for GpuShaderStorageBuffer { @@ -111,6 +112,22 @@ impl RenderAsset for GpuShaderStorageBuffer { source_asset.asset_usage } + fn take_gpu_data( + source: &mut Self::SourceAsset, + previous_gpu_asset: Option<&Self>, + ) -> Result { + let data = source.data.take(); + + let valid_upload = data.is_some() || previous_gpu_asset.is_none_or(|prev| !prev.had_data); + + valid_upload + .then(|| Self::SourceAsset { + data, + ..source.clone() + }) + .ok_or(AssetExtractionError::AlreadyExtracted) + } + fn prepare_asset( source_asset: Self::SourceAsset, _: AssetId, @@ -124,11 +141,17 @@ impl RenderAsset for GpuShaderStorageBuffer { contents: &data, usage: source_asset.buffer_description.usage, }); - Ok(GpuShaderStorageBuffer { buffer }) + Ok(GpuShaderStorageBuffer { + buffer, + had_data: true, + }) } None => { let buffer = render_device.create_buffer(&source_asset.buffer_description); - Ok(GpuShaderStorageBuffer { buffer }) + Ok(GpuShaderStorageBuffer { + buffer, + had_data: false, + }) } } } diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index c6d4c271f958c..738c84bcb9241 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -139,6 +139,7 @@ fn fallback_image_new( sampler, size: image.texture_descriptor.size, mip_level_count: image.texture_descriptor.mip_level_count, + had_data: true, } } diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs index 92e9d5b2be9b3..43483fc993d1d 100644 --- a/crates/bevy_render/src/texture/gpu_image.rs +++ b/crates/bevy_render/src/texture/gpu_image.rs @@ -1,5 +1,5 @@ use crate::{ - render_asset::{PrepareAssetError, RenderAsset}, + render_asset::{AssetExtractionError, PrepareAssetError, RenderAsset}, render_resource::{DefaultImageSampler, Sampler, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, }; @@ -21,6 +21,7 @@ pub struct GpuImage { pub sampler: Sampler, pub size: Extent3d, pub mip_level_count: u32, + pub had_data: bool, } impl RenderAsset for GpuImage { @@ -36,6 +37,24 @@ impl RenderAsset for GpuImage { image.asset_usage } + fn take_gpu_data( + source: &mut Self::SourceAsset, + previous_gpu_asset: Option<&Self>, + ) -> Result { + let data = source.data.take(); + + // check if this image originally had data and no longer does, that implies it + // has already been extracted + let valid_upload = data.is_some() || previous_gpu_asset.is_none_or(|prev| !prev.had_data); + + valid_upload + .then(|| Self::SourceAsset { + data, + ..source.clone() + }) + .ok_or(AssetExtractionError::AlreadyExtracted) + } + #[inline] fn byte_len(image: &Self::SourceAsset) -> Option { image.data.as_ref().map(Vec::len) @@ -48,6 +67,7 @@ impl RenderAsset for GpuImage { (render_device, render_queue, default_sampler): &mut SystemParamItem, previous_asset: Option<&Self>, ) -> Result> { + let had_data = image.data.is_some(); let texture = if let Some(ref data) = image.data { render_device.create_texture_with_data( render_queue, @@ -111,6 +131,7 @@ impl RenderAsset for GpuImage { sampler, size: image.texture_descriptor.size, mip_level_count: image.texture_descriptor.mip_level_count, + had_data, }) } } From be4f4e762afaf31fc1ab09cf2bf6a9a1cff6c7c0 Mon Sep 17 00:00:00 2001 From: momoluna <32905054+momoluna444@users.noreply.github.com> Date: Mon, 15 Dec 2025 06:07:06 +0800 Subject: [PATCH 108/133] Fix shadow caster/receiver toggle not working (#22004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #21582 ## Solution In the original implementation of `extract_meshes_for_gpu_building`, we didn’t account for the case where components are removed. This patch adds fixes for all affected components. ## Testing The example `shadow_caster_receiver` works now. --- crates/bevy_pbr/src/render/mesh.rs | 51 ++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index ecb4b5c543560..e0243899f6b8d 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -17,8 +17,10 @@ use bevy_core_pipeline::{ use bevy_derive::{Deref, DerefMut}; use bevy_diagnostic::FrameCount; use bevy_ecs::{ + entity::EntityHashSet, prelude::*, query::{QueryData, ROQueryItem}, + relationship::RelationshipSourceCollection, system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_image::{BevyDefault, ImageSampler, TextureFormatPixelInfo}; @@ -1507,11 +1509,39 @@ pub fn extract_meshes_for_gpu_building( )>, >, >, + ( + mut removed_previous_global_transform_query, + mut removed_lightmap_query, + mut removed_aabb_query, + mut removed_mesh_tag_query, + mut removed_no_frustum_culling_query, + mut removed_not_shadow_receiver_query, + mut removed_transmitted_receiver_query, + mut removed_not_shadow_caster_query, + mut removed_no_automatic_batching_query, + mut removed_visibility_range_query, + mut removed_skinned_mesh_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), all_meshes_query: Extract>, mut removed_meshes_query: Extract>, gpu_culling_query: Extract, Without)>>, meshes_to_reextract_next_frame: ResMut, + mut reextract_entities: Local, ) { + reextract_entities.clear(); + let any_gpu_culling = !gpu_culling_query.is_empty(); for render_mesh_instance_queue in render_mesh_instance_queues.iter_mut() { @@ -1549,9 +1579,26 @@ pub fn extract_meshes_for_gpu_building( // because the material hadn't yet been loaded. We reextract such materials // on subsequent frames so that `collect_meshes_for_gpu_building` will check // to see if their materials have been prepared. + let iters = meshes_to_reextract_next_frame + .iter() + .map(|&e| *e) + .chain(removed_previous_global_transform_query.read()) + .chain(removed_lightmap_query.read()) + .chain(removed_aabb_query.read()) + .chain(removed_mesh_tag_query.read()) + .chain(removed_no_frustum_culling_query.read()) + .chain(removed_not_shadow_receiver_query.read()) + .chain(removed_transmitted_receiver_query.read()) + .chain(removed_not_shadow_caster_query.read()) + .chain(removed_no_automatic_batching_query.read()) + .chain(removed_visibility_range_query.read()) + .chain(removed_skinned_mesh_query.read()); + + reextract_entities.extend_from_iter(iters); + let mut queue = render_mesh_instance_queues.borrow_local_mut(); - for &mesh_entity in &**meshes_to_reextract_next_frame { - if let Ok(query_row) = all_meshes_query.get(*mesh_entity) { + for entity in &reextract_entities { + if let Ok(query_row) = all_meshes_query.get(*entity) { extract_mesh_for_gpu_building( query_row, &render_visibility_ranges, From fa12668fa027904c89e840babbc7f50d9cc2a9b8 Mon Sep 17 00:00:00 2001 From: Janis <130913856+janis-bhm@users.noreply.github.com> Date: Sun, 14 Dec 2025 23:34:36 +0100 Subject: [PATCH 109/133] system combinators short circuiting with system failure (#20671) # Objective Fixes https://github.com/bevyengine/bevy/issues/20376: CombinatorSystems short circuit if one of the systems fails to validate, e.g. because it queries a Component that isn't resident in the world. ## Solution Instead, we can treat the system failing as the system yielding `false`, appropriately querying the second system depending on the logical operator. ## Testing I added a new test that calls the combinators in different configurations with a failing system as both the rhs and lhs of the combinator and counts the invocations of the system and the condition. --- --------- Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/schedule/condition.rs | 262 +++++++++++++++++- crates/bevy_ecs/src/system/combinator.rs | 58 +++- .../migration-guides/combinator_system.md | 38 +++ 3 files changed, 336 insertions(+), 22 deletions(-) create mode 100644 release-content/migration-guides/combinator_system.md diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index c6a5c99b8cc68..f9963a5200bc7 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1133,7 +1133,7 @@ where a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(a(input, data)? && b(input, data)?) + Ok(a(input, data).unwrap_or(false) && b(input, data).unwrap_or(false)) } } @@ -1155,7 +1155,7 @@ where a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(!(a(input, data)? && b(input, data)?)) + Ok(!(a(input, data).unwrap_or(false) && b(input, data).unwrap_or(false))) } } @@ -1177,7 +1177,7 @@ where a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(!(a(input, data)? || b(input, data)?)) + Ok(!(a(input, data).unwrap_or(false) || b(input, data).unwrap_or(false))) } } @@ -1199,7 +1199,7 @@ where a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(a(input, data)? || b(input, data)?) + Ok(a(input, data).unwrap_or(false) || b(input, data).unwrap_or(false)) } } @@ -1221,7 +1221,7 @@ where a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(!(a(input, data)? ^ b(input, data)?)) + Ok(!(a(input, data).unwrap_or(false) ^ b(input, data).unwrap_or(false))) } } @@ -1243,7 +1243,7 @@ where a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(a(input, data)? ^ b(input, data)?) + Ok(a(input, data).unwrap_or(false) ^ b(input, data).unwrap_or(false)) } } @@ -1252,12 +1252,12 @@ mod tests { use super::{common_conditions::*, SystemCondition}; use crate::error::{BevyError, DefaultErrorHandler, ErrorContext}; use crate::{ - change_detection::ResMut, + change_detection::{Res, ResMut}, component::Component, message::Message, query::With, schedule::{IntoScheduleConfigs, Schedule}, - system::{IntoSystem, Local, Res, System}, + system::{IntoSystem, Local, System}, world::World, }; use bevy_ecs_macros::{Resource, SystemSet}; @@ -1305,6 +1305,252 @@ mod tests { assert_eq!(world.resource::().0, 6); } + #[test] + fn combinators_with_maybe_failing_condition() { + #![allow( + clippy::nonminimal_bool, + clippy::overly_complex_bool_expr, + reason = "Trailing `|| false` and `&& true` are used in this test to visually remain consistent with the combinators" + )] + + use crate::system::RunSystemOnce; + use alloc::sync::Arc; + use std::sync::Mutex; + + // Things that should be tested: + // - the final result of the combinator is correct + // - the systems that are expected to run do run + // - the systems that are expected to not run do not run + + #[derive(Component)] + struct Vacant; + + // SystemConditions don't have mutable access to the world, so we use a + // `Res` to count invocations. + #[derive(Resource, Default)] + struct Counter(Arc>); + + // The following constants are used to represent a system having run. + // both are prime so that when multiplied they give a unique value for any TRUE^n*FALSE^m + const FALSE: usize = 2; + const TRUE: usize = 3; + + // this is a system, but has the same side effect as `test_true` + fn is_true_inc(counter: Res) -> bool { + test_true(&counter) + } + + // this is a system, but has the same side effect as `test_false` + fn is_false_inc(counter: Res) -> bool { + test_false(&counter) + } + + // This condition will always yield `false`, because `Vacant` is never present. + fn vacant(_: crate::system::Single<&Vacant>) -> bool { + true + } + + fn test_true(counter: &Counter) -> bool { + *counter.0.lock().unwrap() *= TRUE; + true + } + + fn test_false(counter: &Counter) -> bool { + *counter.0.lock().unwrap() *= FALSE; + false + } + + // Helper function that runs a logic call and returns the result, as + // well as the composite number of the calls. + fn logic_call_result(f: impl FnOnce(&Counter) -> bool) -> (usize, bool) { + let counter = Counter(Arc::new(Mutex::new(1))); + let result = f(&counter); + (*counter.0.lock().unwrap(), result) + } + + // `test_true` and `test_false` can't fail like the systems can, and so + // we use them to model the short circuiting behavior of rust's logical + // operators. The goal is to end up with a composite number that + // describes rust's behavior and compare that to the result of the + // combinators. + + // we expect `true() || false()` to yield `true`, and short circuit + // after `true()` + assert_eq!( + logic_call_result(|c| test_true(c) || test_false(c)), + (TRUE.pow(1) * FALSE.pow(0), true) + ); + + let mut world = World::new(); + world.init_resource::(); + + // ensure there are no `Vacant` entities + assert!(world.query::<&Vacant>().iter(&world).next().is_none()); + assert!(matches!( + world.run_system_once((|| true).or(vacant)), + Ok(true) + )); + + // This system should fail + assert!(RunSystemOnce::run_system_once(&mut world, vacant).is_err()); + + #[track_caller] + fn assert_system( + world: &mut World, + system: impl IntoSystem<(), bool, Marker>, + equivalent_to: impl FnOnce(&Counter) -> bool, + ) { + use crate::system::System; + + *world.resource::().0.lock().unwrap() = 1; + + let system = IntoSystem::into_system(system); + let name = system.name(); + + let out = RunSystemOnce::run_system_once(&mut *world, system).unwrap_or(false); + + let (expected_counter, expected) = logic_call_result(equivalent_to); + let caller = core::panic::Location::caller(); + let counter = *world.resource::().0.lock().unwrap(); + + assert_eq!( + out, + expected, + "At {}:{} System `{name}` yielded unexpected value `{out}`, expected `{expected}`", + caller.file(), + caller.line(), + ); + + assert_eq!( + counter, expected_counter, + "At {}:{} System `{name}` did not increment counter as expected: expected `{expected_counter}`, got `{counter}`", + caller.file(), + caller.line(), + ); + } + + assert_system(&mut world, is_true_inc.or(vacant), |c| { + test_true(c) || false + }); + assert_system(&mut world, is_true_inc.nor(vacant), |c| { + !(test_true(c) || false) + }); + assert_system(&mut world, is_true_inc.xor(vacant), |c| { + test_true(c) ^ false + }); + assert_system(&mut world, is_true_inc.xnor(vacant), |c| { + !(test_true(c) ^ false) + }); + assert_system(&mut world, is_true_inc.and(vacant), |c| { + test_true(c) && false + }); + assert_system(&mut world, is_true_inc.nand(vacant), |c| { + !(test_true(c) && false) + }); + + // even if `vacant` fails as the first condition, where applicable (or, + // xor), `is_true_inc` should still be called. `and` and `nand` short + // circuit on an initial `false`. + assert_system(&mut world, vacant.or(is_true_inc), |c| { + false || test_true(c) + }); + assert_system(&mut world, vacant.nor(is_true_inc), |c| { + !(false || test_true(c)) + }); + assert_system(&mut world, vacant.xor(is_true_inc), |c| { + false ^ test_true(c) + }); + assert_system(&mut world, vacant.xnor(is_true_inc), |c| { + !(false ^ test_true(c)) + }); + assert_system(&mut world, vacant.and(is_true_inc), |c| { + false && test_true(c) + }); + assert_system(&mut world, vacant.nand(is_true_inc), |c| { + !(false && test_true(c)) + }); + + // the same logic ought to be the case with a condition that runs, but yields `false`: + assert_system(&mut world, is_true_inc.or(is_false_inc), |c| { + test_true(c) || test_false(c) + }); + assert_system(&mut world, is_true_inc.nor(is_false_inc), |c| { + !(test_true(c) || test_false(c)) + }); + assert_system(&mut world, is_true_inc.xor(is_false_inc), |c| { + test_true(c) ^ test_false(c) + }); + assert_system(&mut world, is_true_inc.xnor(is_false_inc), |c| { + !(test_true(c) ^ test_false(c)) + }); + assert_system(&mut world, is_true_inc.and(is_false_inc), |c| { + test_true(c) && test_false(c) + }); + assert_system(&mut world, is_true_inc.nand(is_false_inc), |c| { + !(test_true(c) && test_false(c)) + }); + + // and where one condition yields `false` and the other fails: + assert_system(&mut world, is_false_inc.or(vacant), |c| { + test_false(c) || false + }); + assert_system(&mut world, is_false_inc.nor(vacant), |c| { + !(test_false(c) || false) + }); + assert_system(&mut world, is_false_inc.xor(vacant), |c| { + test_false(c) ^ false + }); + assert_system(&mut world, is_false_inc.xnor(vacant), |c| { + !(test_false(c) ^ false) + }); + assert_system(&mut world, is_false_inc.and(vacant), |c| { + test_false(c) && false + }); + assert_system(&mut world, is_false_inc.nand(vacant), |c| { + !(test_false(c) && false) + }); + + // and where both conditions yield `true`: + assert_system(&mut world, is_true_inc.or(is_true_inc), |c| { + test_true(c) || test_true(c) + }); + assert_system(&mut world, is_true_inc.nor(is_true_inc), |c| { + !(test_true(c) || test_true(c)) + }); + assert_system(&mut world, is_true_inc.xor(is_true_inc), |c| { + test_true(c) ^ test_true(c) + }); + assert_system(&mut world, is_true_inc.xnor(is_true_inc), |c| { + !(test_true(c) ^ test_true(c)) + }); + assert_system(&mut world, is_true_inc.and(is_true_inc), |c| { + test_true(c) && test_true(c) + }); + assert_system(&mut world, is_true_inc.nand(is_true_inc), |c| { + !(test_true(c) && test_true(c)) + }); + + // and where both conditions yield `false`: + assert_system(&mut world, is_false_inc.or(is_false_inc), |c| { + test_false(c) || test_false(c) + }); + assert_system(&mut world, is_false_inc.nor(is_false_inc), |c| { + !(test_false(c) || test_false(c)) + }); + assert_system(&mut world, is_false_inc.xor(is_false_inc), |c| { + test_false(c) ^ test_false(c) + }); + assert_system(&mut world, is_false_inc.xnor(is_false_inc), |c| { + !(test_false(c) ^ test_false(c)) + }); + assert_system(&mut world, is_false_inc.and(is_false_inc), |c| { + test_false(c) && test_false(c) + }); + assert_system(&mut world, is_false_inc.nand(is_false_inc), |c| { + !(test_false(c) && test_false(c)) + }); + } + #[test] fn run_condition_combinators() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index a12b3469a4651..b05711569ea0b 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -4,6 +4,7 @@ use core::marker::PhantomData; use crate::{ change_detection::{CheckChangeTicks, Tick}, + error::ErrorContext, prelude::World, query::FilteredAccessSet, schedule::InternedSystemSet, @@ -157,6 +158,42 @@ where ) -> Result { struct PrivateUnsafeWorldCell<'w>(UnsafeWorldCell<'w>); + // Since control over handling system run errors is passed on to the + // implementation of `Func::combine`, which may run the two closures + // however it wants, errors must be intercepted here if they should be + // handled by the world's error handler. + unsafe fn run_system( + system: &mut S, + input: SystemIn, + world: &mut PrivateUnsafeWorldCell, + ) -> Result { + // SAFETY: see comment on `Func::combine` call + match (|| unsafe { + system.validate_param_unsafe(world.0)?; + system.run_unsafe(input, world.0) + })() { + Err(RunSystemError::Failed(err)) => { + // let the world's default error handler handle the error if `Failed(_)` + (world.0.default_error_handler())( + err, + ErrorContext::System { + name: system.name(), + last_run: system.get_last_run(), + }, + ); + + // Since the error handler takes the error by value, create a new error: + // The original error has already been handled, including + // the reason for the failure here isn't important. + Err(format!("System `{}` failed", system.name()).into()) + } + // `Skipped(_)` and `Ok(_)` are passed through: + // system skipping is not an error, and isn't passed to the + // world's error handler by the executors. + result @ (Ok(_) | Err(RunSystemError::Skipped(_))) => result, + } + } + Func::combine( input, &mut PrivateUnsafeWorldCell(world), @@ -167,14 +204,9 @@ where // passed to a function as an unbound non-'static generic argument, they can never be called in parallel // or re-entrantly because that would require forging another instance of `PrivateUnsafeWorldCell`. // This means that the world accesses in the two closures will not conflict with each other. - |input, world| unsafe { self.a.run_unsafe(input, world.0) }, - // `Self::validate_param_unsafe` already validated the first system, - // but we still need to validate the second system once the first one runs. + |input, world| unsafe { run_system(&mut self.a, input, world) }, // SAFETY: See the comment above. - |input, world| unsafe { - self.b.validate_param_unsafe(world.0)?; - self.b.run_unsafe(input, world.0) - }, + |input, world| unsafe { run_system(&mut self.b, input, world) }, ) } @@ -200,14 +232,12 @@ where #[inline] unsafe fn validate_param_unsafe( &mut self, - world: UnsafeWorldCell, + _world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - // We only validate parameters for the first system, - // since it may make changes to the world that affect - // whether the second system has valid parameters. - // The second system will be validated in `Self::run_unsafe`. - // SAFETY: Delegate to other `System` implementations. - unsafe { self.a.validate_param_unsafe(world) } + // Both systems are validated in `Self::run_unsafe`, so that we get the + // chance to run the second system even if the first one fails to + // validate. + Ok(()) } fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { diff --git a/release-content/migration-guides/combinator_system.md b/release-content/migration-guides/combinator_system.md new file mode 100644 index 0000000000000..7af01c344e41c --- /dev/null +++ b/release-content/migration-guides/combinator_system.md @@ -0,0 +1,38 @@ +--- +title: System Combinators +pull_requests: [20671] +--- + +The `CombinatorSystem`s can be used to combine multiple `SystemCondition`s with logical operators. Previously, the conditions would short circuit if the system failed to run, for example because it's query could not be filled by the world. + +Now, the `CombinatorSystem`s will work as expected, following the semantics of rust's logical operators. +Namely, if a `SystemCondition` fails, it will be considered to have returned `false` and in combinators that don't short circuit the other condition will now be run. + +Specifically, the combinators act as follows: + +| Combinator | Rust Equivalent | +|:----------:|:---------------:| +| `and` | `a && b` | +| `or` | `a \|\| b` | +| `xor` | `a ^ b` | +| `nand` | `!(a && b)` | +| `nor` | `!(a \|\| b)` | +| `xnor` | `!(a ^ b)` | + +```rust +fn vacant(_: crate::system::Single<&Vacant>) -> bool { + true +} + +fn is_true() -> bool { + true +} + +assert!(world.query::<&Vacant>().iter(&world).next().is_none()); + +// previously: +assert!(world.run_system_once(is_true.or(vacant)).is_err()); + +// now: +assert!(matches!(world.run_system_once(is_true.or(vacant)), Ok(true))); +``` From 681751647a588b5500ad0b9fe5cd2690c10b1003 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Sun, 14 Dec 2025 17:35:20 -0500 Subject: [PATCH 110/133] Add FullscreenMaterial (#20414) # Objective - Users often want to run a fullscreen shader but the current solution involves copying the custom_post_processing example which is a 350 line file with a lot of low level wgpu complexity. Users shouldn't have to deal with that just to make a fullscreen shader ## Solution - Introduce a new FullscreenMaterial trait and FullscsreenMaterialPlugin - This new material will run a fullscreen triangle with the specified shader. It builds on top of the existing FullscreenShader infrastructure - It lets user customize the node ordering. There's no defaults right now becausae it's intended as a bit of a primitive plugin. Eventually we could have some kind of default for custom post processing ## Testing Made a new fullscreen_material example and made sure it works ## Follow up Once this is merged there are various things that should be done to improve it. Add the option to bind the depth texture, offer defaults for post processing, use a full AsBindGroup, add a way to bind the gbuffer. --------- Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> Co-authored-by: Alice Cecile --- Cargo.toml | 17 +- assets/shaders/fullscreen_effect.wgsl | 50 +++ .../src/fullscreen_material.rs | 329 ++++++++++++++++++ crates/bevy_core_pipeline/src/lib.rs | 1 + crates/bevy_render/src/render_graph/node.rs | 6 + examples/README.md | 7 + .../shader_advanced/fullscreen_material.rs | 89 +++++ .../release-notes/fullscreen_material.md | 7 + 8 files changed, 503 insertions(+), 3 deletions(-) create mode 100644 assets/shaders/fullscreen_effect.wgsl create mode 100644 crates/bevy_core_pipeline/src/fullscreen_material.rs create mode 100644 examples/shader_advanced/fullscreen_material.rs create mode 100644 release-content/release-notes/fullscreen_material.md diff --git a/Cargo.toml b/Cargo.toml index c3d1dfb93f35f..61d0d5a580a80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3113,6 +3113,17 @@ description = "Demonstrates how to write a specialized mesh pipeline" category = "Shaders" wasm = true +[[example]] +name = "fullscreen_material" +path = "examples/shader_advanced/fullscreen_material.rs" +doc-scrape-examples = true + +[package.metadata.example.fullscreen_material] +name = "Fullscreen Material" +description = "Demonstrates how to write a fullscreen material" +category = "Shaders Advanced" +wasm = true + # Stress tests [[package.metadata.example_category]] name = "Stress Tests" @@ -3992,6 +4003,9 @@ name = "fallback_image" path = "examples/shader/fallback_image.rs" doc-scrape-examples = true +[package.metadata.example.fallback_image] +hidden = true + [[example]] name = "reflection_probes" path = "examples/3d/reflection_probes.rs" @@ -4003,9 +4017,6 @@ description = "Demonstrates reflection probes" category = "3D Rendering" wasm = false -[package.metadata.example.fallback_image] -hidden = true - [package.metadata.example.window_resizing] name = "Window Resizing" description = "Demonstrates resizing and responding to resizing a window" diff --git a/assets/shaders/fullscreen_effect.wgsl b/assets/shaders/fullscreen_effect.wgsl new file mode 100644 index 0000000000000..b4a88cff94da2 --- /dev/null +++ b/assets/shaders/fullscreen_effect.wgsl @@ -0,0 +1,50 @@ +// This shader computes the chromatic aberration effect + +// Since post processing is a fullscreen effect, we use the fullscreen vertex shader provided by bevy. +// This will import a vertex shader that renders a single fullscreen triangle. +// +// A fullscreen triangle is a single triangle that covers the entire screen. +// The box in the top left in that diagram is the screen. The 4 x are the corner of the screen +// +// Y axis +// 1 | x-----x...... +// 0 | | s | . ´ +// -1 | x_____x´ +// -2 | : .´ +// -3 | :´ +// +--------------- X axis +// -1 0 1 2 3 +// +// As you can see, the triangle ends up bigger than the screen. +// +// You don't need to worry about this too much since bevy will compute the correct UVs for you. +#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput + +@group(0) @binding(0) var screen_texture: texture_2d; +@group(0) @binding(1) var texture_sampler: sampler; + +struct FullScreenEffect { + intensity: f32, +#ifdef SIXTEEN_BYTE_ALIGNMENT + // WebGL2 structs must be 16 byte aligned. + _webgl2_padding: vec3 +#endif +} + +@group(0) @binding(2) var settings: FullScreenEffect; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Chromatic aberration strength + let offset_strength = settings.intensity; + + // Sample each color channel with an arbitrary shift + return vec4( + textureSample(screen_texture, texture_sampler, in.uv + vec2(offset_strength, -offset_strength)).r, + textureSample(screen_texture, texture_sampler, in.uv + vec2(-offset_strength, 0.0)).g, + textureSample(screen_texture, texture_sampler, in.uv + vec2(0.0, offset_strength)).b, + 1.0 + ); +} + + diff --git a/crates/bevy_core_pipeline/src/fullscreen_material.rs b/crates/bevy_core_pipeline/src/fullscreen_material.rs new file mode 100644 index 0000000000000..4de57f54cbfc9 --- /dev/null +++ b/crates/bevy_core_pipeline/src/fullscreen_material.rs @@ -0,0 +1,329 @@ +//! This is mostly a pluginified version of the `custom_post_processing` example +//! +//! The plugin will create a new node that runs a fullscreen triangle. +//! +//! Users need to use the [`FullscreenMaterial`] trait to define the parameters like the graph label or the graph ordering. + +use core::any::type_name; +use core::marker::PhantomData; + +use crate::{core_2d::graph::Core2d, core_3d::graph::Core3d, FullscreenShader}; +use bevy_app::{App, Plugin}; +use bevy_asset::AssetServer; +use bevy_camera::{Camera2d, Camera3d}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{Added, Has, QueryItem}, + resource::Resource, + system::{Commands, Res}, + world::{FromWorld, World}, +}; +use bevy_image::BevyDefault; +use bevy_render::{ + extract_component::{ + ComponentUniforms, DynamicUniformIndex, ExtractComponent, ExtractComponentPlugin, + UniformComponentPlugin, + }, + render_graph::{ + InternedRenderLabel, InternedRenderSubGraph, NodeRunError, RenderGraph, RenderGraphContext, + RenderGraphError, RenderGraphExt, RenderLabel, ViewNode, ViewNodeRunner, + }, + render_resource::{ + binding_types::{sampler, texture_2d, uniform_buffer}, + encase::internal::WriteInto, + BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, + CachedRenderPipelineId, ColorTargetState, ColorWrites, FragmentState, Operations, + PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, RenderPipelineDescriptor, + Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, ShaderType, TextureFormat, + TextureSampleType, + }, + renderer::{RenderContext, RenderDevice}, + view::ViewTarget, + ExtractSchedule, MainWorld, RenderApp, RenderStartup, +}; +use bevy_shader::ShaderRef; +use bevy_utils::default; +use tracing::warn; + +#[derive(Default)] +pub struct FullscreenMaterialPlugin { + _marker: PhantomData, +} +impl Plugin for FullscreenMaterialPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ExtractComponentPlugin::::default(), + UniformComponentPlugin::::default(), + )); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app.add_systems(RenderStartup, init_pipeline::); + + if let Some(sub_graph) = T::sub_graph() { + render_app.add_render_graph_node::>>( + sub_graph, + T::node_label(), + ); + + // We can't use add_render_graph_edges because it doesn't accept a Vec + if let Some(mut render_graph) = render_app.world_mut().get_resource_mut::() + && let Some(graph) = render_graph.get_sub_graph_mut(sub_graph) + { + for window in T::node_edges().windows(2) { + let [a, b] = window else { + break; + }; + let Err(err) = graph.try_add_node_edge(*a, *b) else { + continue; + }; + match err { + // Already existing edges are very easy to produce with this api + // and shouldn't cause a panic + RenderGraphError::EdgeAlreadyExists(_) => {} + _ => panic!("{err:?}"), + } + } + } else { + warn!("Failed to add edges for FullscreenMaterial"); + }; + } else { + // If there was no sub_graph specified we try to determine the graph based on the camera + // it gets added to. + render_app.add_systems(ExtractSchedule, extract_on_add::); + } + } +} + +fn extract_on_add(world: &mut World) { + world.resource_scope::(|world, mut main_world| { + // Extract the material from the main world + let mut query = + main_world.query_filtered::<(Entity, Has, Has), Added>(); + + // Create the node and add it to the render graph + world.resource_scope::(|world, mut render_graph| { + for (_entity, is_3d, is_2d) in query.iter(&main_world) { + let graph = if is_3d && let Some(graph) = render_graph.get_sub_graph_mut(Core3d) { + graph + } else if is_2d && let Some(graph) = render_graph.get_sub_graph_mut(Core2d) { + graph + } else { + warn!("FullscreenMaterial was added to an entity that isn't a camera"); + continue; + }; + + let node = ViewNodeRunner::>::from_world(world); + graph.add_node(T::node_label(), node); + + for window in T::node_edges().windows(2) { + let [a, b] = window else { + break; + }; + let Err(err) = graph.try_add_node_edge(*a, *b) else { + continue; + }; + match err { + // Already existing edges are very easy to produce with this api + // and shouldn't cause a panic + RenderGraphError::EdgeAlreadyExists(_) => {} + _ => panic!("{err:?}"), + } + } + } + }); + }); +} + +/// A trait to define a material that will render to the entire screen using a fullscrene triangle +pub trait FullscreenMaterial: + Component + ExtractComponent + Clone + Copy + ShaderType + WriteInto + Default +{ + /// The shader that will run on the entire screen using a fullscreen triangle + fn fragment_shader() -> ShaderRef; + + /// The list of `node_edges`. In 3d, for a post processing effect, it would look like this: + /// + /// ```compile_fail + /// # use bevy_core_pipeline::core_3d::graph::Node3d; + /// # use bevy_render::render_graph::RenderLabel; + /// vec![ + /// Node3d::Tonemapping.intern(), + /// // Self::sub_graph().intern(), // <--- your own label here + /// Node3d::EndMainPassPostProcessing.intern(), + /// ] + /// ``` + /// + /// This tell the render graph to run your fullscreen effect after the tonemapping pass but + /// before the end of post processing. For 2d, it would be the same but using Node2d. You can + /// specify any edges you want but make sure to include your own label. + fn node_edges() -> Vec; + + /// The [`bevy_render::render_graph::RenderSubGraph`] the effect will run in + /// + /// For 2d this is generally [`crate::core_2d::graph::Core2d`] and for 3d it's + /// [`crate::core_3d::graph::Core3d`] + fn sub_graph() -> Option { + None + } + + /// The label used to represent the render node that will run the pass + fn node_label() -> impl RenderLabel { + FullscreenMaterialLabel(type_name::()) + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone)] +struct FullscreenMaterialLabel(&'static str); + +impl RenderLabel for FullscreenMaterialLabel +where + Self: 'static + Send + Sync + Clone + Eq + ::core::fmt::Debug + ::core::hash::Hash, +{ + fn dyn_clone(&self) -> Box { + Box::new(::core::clone::Clone::clone(self)) + } +} + +#[derive(Resource)] +struct FullscreenMaterialPipeline { + layout: BindGroupLayoutDescriptor, + sampler: Sampler, + pipeline_id: CachedRenderPipelineId, + pipeline_id_hdr: CachedRenderPipelineId, +} + +fn init_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, + fullscreen_shader: Res, + pipeline_cache: Res, +) { + let layout = BindGroupLayoutDescriptor::new( + "post_process_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + // The screen texture + texture_2d(TextureSampleType::Float { filterable: true }), + // The sampler that will be used to sample the screen texture + sampler(SamplerBindingType::Filtering), + // We use a uniform buffer so users can pass some data to the effect + // Eventually we should just use a separate bind group for user data + uniform_buffer::(true), + ), + ), + ); + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + let shader = match T::fragment_shader() { + ShaderRef::Default => { + // TODO not sure what an actual fallback should be. An empty shader or output a solid + // color to indicate a missing shader? + unimplemented!( + "FullscreenMaterial::fragment_shader() must not return ShaderRef::Default" + ) + } + ShaderRef::Handle(handle) => handle, + ShaderRef::Path(path) => asset_server.load(path), + }; + // Setup a fullscreen triangle for the vertex state. + let vertex_state = fullscreen_shader.to_vertex_state(); + let mut desc = RenderPipelineDescriptor { + label: Some("post_process_pipeline".into()), + layout: vec![layout.clone()], + vertex: vertex_state, + fragment: Some(FragmentState { + shader, + targets: vec![Some(ColorTargetState { + format: TextureFormat::bevy_default(), + blend: None, + write_mask: ColorWrites::ALL, + })], + ..default() + }), + ..default() + }; + let pipeline_id = pipeline_cache.queue_render_pipeline(desc.clone()); + desc.fragment.as_mut().unwrap().targets[0] + .as_mut() + .unwrap() + .format = ViewTarget::TEXTURE_FORMAT_HDR; + let pipeline_id_hdr = pipeline_cache.queue_render_pipeline(desc); + commands.insert_resource(FullscreenMaterialPipeline { + layout, + sampler, + pipeline_id, + pipeline_id_hdr, + }); +} + +#[derive(Default)] +struct FullscreenMaterialNode { + _marker: PhantomData, +} + +impl ViewNode for FullscreenMaterialNode { + // TODO we should expose the depth buffer and the gbuffer if using deferred + type ViewQuery = (&'static ViewTarget, &'static DynamicUniformIndex); + + fn run<'w>( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (view_target, settings_index): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let fullscreen_pipeline = world.resource::(); + + let pipeline_cache = world.resource::(); + let pipeline_id = if view_target.is_hdr() { + fullscreen_pipeline.pipeline_id_hdr + } else { + fullscreen_pipeline.pipeline_id + }; + + let Some(pipeline) = pipeline_cache.get_render_pipeline(pipeline_id) else { + return Ok(()); + }; + + let data_uniforms = world.resource::>(); + let Some(settings_binding) = data_uniforms.uniforms().binding() else { + return Ok(()); + }; + + let post_process = view_target.post_process_write(); + + let bind_group = render_context.render_device().create_bind_group( + "post_process_bind_group", + &pipeline_cache.get_bind_group_layout(&fullscreen_pipeline.layout), + &BindGroupEntries::sequential(( + post_process.source, + &fullscreen_pipeline.sampler, + settings_binding.clone(), + )), + ); + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("post_process_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: post_process.destination, + depth_slice: None, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + render_pass.set_render_pipeline(pipeline); + render_pass.set_bind_group(0, &bind_group, &[settings_index.index()]); + render_pass.draw(0..3, 0..1); + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 0f084152d8665..6037e89cc325d 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -11,6 +11,7 @@ pub mod core_2d; pub mod core_3d; pub mod deferred; pub mod experimental; +pub mod fullscreen_material; pub mod oit; pub mod prepass; pub mod tonemapping; diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index c75218c0a2941..bfc007b12754a 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -38,6 +38,12 @@ pub trait IntoRenderNodeArray { fn into_array(self) -> [InternedRenderLabel; N]; } +impl IntoRenderNodeArray for Vec { + fn into_array(self) -> [InternedRenderLabel; N] { + self.try_into().unwrap() + } +} + macro_rules! impl_render_label_tuples { ($N: expr, $(#[$meta:meta])* $(($T: ident, $I: ident)),*) => { $(#[$meta])* diff --git a/examples/README.md b/examples/README.md index 52f0379ea915f..06e451d4f5c91 100644 --- a/examples/README.md +++ b/examples/README.md @@ -62,6 +62,7 @@ git checkout v0.4.0 - [Scene](#scene) - [Shader Advanced](#shader-advanced) - [Shaders](#shaders) + - [Shaders Advanced](#shaders-advanced) - [State](#state) - [Stress Tests](#stress-tests) - [Time](#time) @@ -488,6 +489,12 @@ Example | Description [Storage Buffer](../examples/shader/storage_buffer.rs) | A shader that shows how to bind a storage buffer using a custom material. [Texture Binding Array (Bindless Textures)](../examples/shader_advanced/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). +### Shaders Advanced + +Example | Description +--- | --- +[Fullscreen Material](../examples/shader_advanced/fullscreen_material.rs) | Demonstrates how to write a fullscreen material + ### State Example | Description diff --git a/examples/shader_advanced/fullscreen_material.rs b/examples/shader_advanced/fullscreen_material.rs new file mode 100644 index 0000000000000..4cd5ed6da50e7 --- /dev/null +++ b/examples/shader_advanced/fullscreen_material.rs @@ -0,0 +1,89 @@ +//! Demonstrates how to write a custom fullscreen shader +//! +//! This is currently limited to 3d only but work is in progress to make it work in 2d + +use bevy::{ + core_pipeline::{ + core_3d::graph::Node3d, + fullscreen_material::{FullscreenMaterial, FullscreenMaterialPlugin}, + }, + prelude::*, + render::{ + extract_component::ExtractComponent, + render_graph::{InternedRenderLabel, RenderLabel}, + render_resource::ShaderType, + }, + shader::ShaderRef, +}; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + FullscreenMaterialPlugin::::default(), + )) + .add_systems(Startup, setup) + .run(); +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // camera + commands.spawn(( + Camera3d::default(), + Transform::from_translation(Vec3::new(0.0, 0.0, 5.0)).looking_at(Vec3::default(), Vec3::Y), + FullscreenEffect { intensity: 0.005 }, + )); + + // cube + commands.spawn(( + Mesh3d(meshes.add(Cuboid::default())), + MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Transform::default(), + )); + + // light + commands.spawn(DirectionalLight { + illuminance: 1_000., + ..default() + }); +} + +// This is the struct that will be sent to your shader +// +// Currently, this doesn't support AsBindGroup so you can only use it to send a struct to your +// shader. We are working on adding AsBindGroup support in the future so you can bind anything you +// need. +#[derive(Component, ExtractComponent, Clone, Copy, ShaderType, Default)] +struct FullscreenEffect { + // For this example, this is used as the intensity of the effect, but you can pass in any valid + // ShaderType + // + // In the future, you will be able to use a full bind group + intensity: f32, +} + +impl FullscreenMaterial for FullscreenEffect { + // The shader that will be used + fn fragment_shader() -> ShaderRef { + "shaders/fullscreen_effect.wgsl".into() + } + + // This let's you specify a list of edges used to order when your effect pass will run + // + // This example is a post processing effect so it will run after tonemapping but before the end + // post processing pass. + // + // In 2d you would need to use [`Node2d`] instead of [`Node3d`] + fn node_edges() -> Vec { + vec![ + Node3d::Tonemapping.intern(), + // The label is automatically generated from the name of the struct + Self::node_label().intern(), + Node3d::EndMainPassPostProcessing.intern(), + ] + } +} diff --git a/release-content/release-notes/fullscreen_material.md b/release-content/release-notes/fullscreen_material.md new file mode 100644 index 0000000000000..59f0307107982 --- /dev/null +++ b/release-content/release-notes/fullscreen_material.md @@ -0,0 +1,7 @@ +--- +title: Fullscreen Material +authors: ["@IceSentry"] +pull_requests: [20414] +--- + +Users often want to run a fullscreen shader but currently the only to do this is to copy the custom_post_processing example which is very verbose and contains a lot of low level details. We introduced a new `FullscreenMaterial` trait and `FullscreenMaterialPlugin` that let you easily run a fullscreen shader and specify in which order it will run relative to other render passes in the engine. From ac7ee143d6c615b4dfce693562b1a5300b9c371d Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Sun, 14 Dec 2025 14:40:38 -0800 Subject: [PATCH 111/133] bug: Fix stackoverflow on asset reload. (#21619) # Objective Fix the stackoverflow on asset reload when asset contains its own path as a dependency. ## Problem There is a way to create a circular dependency graph with an asset loader. I don't know how I managed to do it. I have tried to create a minimal example, but it has not exhibited the error yet. But I do have a means of exhibiting the error with my project Nano-9, reproduction details below. ## Solution This commit has two fixes: one at the insertion point (introducing self-reference), and one at the recursion point (following self-reference). ### Insertion point Issue warning when an asset wants to mark itself as a dependency and do not allow inserting itself as a dependent. ### Recursion point Check for self loops. Warn on self detection and do not loop. It's likely that if you fix it at the insertion point, you don't need to worry about it at the recursion point. You will have stopped the cause of the issue. I left both in for transparency about where the issue lies so far as I could see. ## Testing I can reproduce this error with an example from my [Nano-9 project](https://github.com/shanecelis/nano-9). I wish it were a minimal example. It's not, but I have put it on a branch to isolate this issue. It uses my Bevy fork that is v0.16.1 plus a commit tagged v0.16.1b, which was required for it to build. This PR is a cherry pick of the fix commit against Bevy's main branch. I can reproduce this error by doing the following: ```sh git clone -b bevy-asset-stackoverflow https://github.com/shanecelis/nano-9.git cd nano-9 cargo run --example sprite --features watcher --no-default-features & touch assets/BirdSprite.png ``` Here is an excerpt of the crash report on macOS 15.6.1, M4 Max: ``` ... Thread 0 Crashed:: main Dispatch queue: com.apple.main-thread 0 libsystem_kernel.dylib 0x18cc2a388 __pthread_kill + 8 1 libsystem_pthread.dylib 0x18cc6388c pthread_kill + 296 2 libsystem_c.dylib 0x18cb6ca3c abort + 124 3 sprite 0x10598d09c std::sys::pal::unix::abort_internal::h1edcc850f5dec78e + 12 4 sprite 0x10598c5b0 std::process::abort::hffd6db68ff0662a6 + 12 5 sprite 0x10580d464 std::sys::pal::unix::stack_overflow::imp::signal_handler::h7b8eae417c5ee98d + 604 6 libsystem_platform.dylib 0x18cc9d6a4 _sigtramp + 56 7 sprite 0x10573f890 _$LT$std..path..Path$u20$as$u20$core..hash..Hash$GT$::hash::h732e05949b6a170e + 136 8 sprite 0x10573f890 _$LT$std..path..Path$u20$as$u20$core..hash..Hash$GT$::hash::h732e05949b6a170e + 136 9 sprite 0x104d5f278 _$LT$atomicow..CowArc$LT$T$GT$$u20$as$u20$core..hash..Hash$GT$::hash::h6c45383281764a05 + 40 10 sprite 0x104ed62d8 _$LT$bevy_asset..path..AssetPath$u20$as$u20$core..hash..Hash$GT$::hash::h11b348528182d76d + 52 11 sprite 0x104ded598 hashbrown::map::make_hash::hb7812997186aa817 + 56 12 sprite 0x104de8eec hashbrown::map::HashMap$LT$K$C$V$C$S$C$A$GT$::insert::h3a8b107dcf9615e9 + 64 13 sprite 0x104da7264 hashbrown::set::HashSet$LT$T$C$S$C$A$GT$::insert::h44e988e7a7752688 + 24 14 sprite 0x104e7b15c bevy_platform::collections::hash_set::HashSet$LT$T$C$S$GT$::insert::hb1d23d506d548fcc + 24 15 sprite 0x104dba770 bevy_asset::server::handle_internal_asset_events::_$u7b$$u7b$closure$u7d$$u7d$::queue_ancestors::hd1670687bb18ae9f + 216 16 sprite 0x104dba780 bevy_asset::server::handle_internal_asset_events::_$u7b$$u7b$closure$u7d$$u7d$::queue_ancestors::hd1670687bb18ae9f + 232 17 sprite 0x104dba780 bevy_asset::server::handle_internal_asset_events::_$u7b$$u7b$closure$u7d$$u7d$::queue_ancestors::hd1670687bb18ae9f + 232 18 sprite 0x104dba780 bevy_asset::server::handle_internal_asset_events::_$u7b$$u7b$closure$u7d$$u7d$::queue_ancestors::hd1670687bb18ae9f + 232 19 sprite 0x104dba780 bevy_asset::server::handle_internal_asset_events::_$u7b$$u7b$closure$u7d$$u7d$::queue_ancestors::hd1670687bb18ae9f + 232 ... ``` ### Exercising the fix Alter Nano-9's Cargo file to use the fix branch: ```diff -bevy = { git = "https://github.com/shanecelis/bevy.git", tag = "v0.16.1b" } +bevy = { git = "https://github.com/shanecelis/bevy.git", branch = "fix/asset-reload-overflow" } ``` Run the same command again, and you will see a warning: ```sh 2025-10-21T04:10:52.348726Z WARN bevy_asset::server::info: Asset 'BirdSprite.png' wants to treat itself as a dependency ``` ## Alternative Solution This commit fixes the immediate stackoverflow issue; however, it would be even better if the user were prevented from making a circular dependency graph in the first place. --------- Co-authored-by: Alice Cecile --- crates/bevy_asset/src/lib.rs | 378 +++++++++++++++++++++++ crates/bevy_asset/src/loader.rs | 11 +- crates/bevy_asset/src/loader_builders.rs | 28 +- crates/bevy_asset/src/server/mod.rs | 7 + 4 files changed, 420 insertions(+), 4 deletions(-) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index cdaf34bffe8c2..d7aa964ef497a 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -1978,6 +1978,384 @@ mod tests { }); } + #[test] + fn error_on_immediate_load_of_self_path() { + let (mut app, dir, _source_events) = create_app_with_source_event_sender(); + let asset_server = app.world().resource::().clone(); + + // Extension "rsp" for Recursive Self Path (RSP). + dir.insert_asset_text(Path::new("abc.rsp"), ""); + + struct ImmediateSelfLoader; + + impl AssetLoader for ImmediateSelfLoader { + type Asset = TestAsset; + type Error = crate::loader::LoadDirectError; + type Settings = (); + + async fn load( + &self, + _: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let asset_path = load_context.path().clone(); + let loaded_asset = load_context + .loader() + .immediate() + .load::(asset_path) + .await?; + load_context.add_loaded_labeled_asset("myself".to_string(), loaded_asset); + Ok(TestAsset) + } + + fn extensions(&self) -> &[&str] { + &["rsp"] + } + } + + app.init_asset::() + .register_asset_loader(ImmediateSelfLoader); + + let handle: Handle = asset_server.load("abc.rsp"); + + run_app_until(&mut app, |_world| match asset_server.load_state(&handle) { + LoadState::Loading => None, + LoadState::Failed(err) => { + assert!( + format!("{:?}", &err).contains("RequestedSelfPath"), + "Error did not contain RequestedSelfPath: {:?}", + &err + ); + Some(()) + } + state => panic!("Unexpected asset state: {state:?}"), + }); + } + + #[test] + fn error_on_unknown_type_immediate_load_of_self_path() { + let (mut app, dir, _source_events) = create_app_with_source_event_sender(); + let asset_server = app.world().resource::().clone(); + + dir.insert_asset_text(Path::new("abc.rsp"), ""); + + struct ImmediateSelfLoader; + + impl AssetLoader for ImmediateSelfLoader { + type Asset = TestAsset; + type Error = crate::loader::LoadDirectError; + type Settings = (); + + async fn load( + &self, + _: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let asset_path = load_context.path().clone(); + let loaded_asset = load_context + .loader() + .immediate() + .with_unknown_type() + .load(asset_path) + .await?; + let Ok(loaded_asset) = loaded_asset.downcast::() else { + panic!("Could not downcast to `TestAsset`") + }; + load_context.add_loaded_labeled_asset("myself".to_string(), loaded_asset); + Ok(TestAsset) + } + + fn extensions(&self) -> &[&str] { + &["rsp"] + } + } + + app.init_asset::() + .register_asset_loader(ImmediateSelfLoader); + + let handle: Handle = asset_server.load("abc.rsp"); + + run_app_until(&mut app, |_world| match asset_server.load_state(&handle) { + LoadState::Loading => None, + LoadState::Failed(err) => { + assert!( + format!("{:?}", &err).contains("RequestedSelfPath"), + "Error did not contain RequestedSelfPath: {:?}", + &err + ); + Some(()) + } + state => panic!("Unexpected asset state: {state:?}"), + }); + } + + /// This is not a statement of intent but of behavior: One may load their + /// own path deferred without error. It has the correct handle to itself. + /// And it can reload. + #[test] + fn no_error_on_deferred_load_of_self_path() { + let (mut app, dir, source_events) = create_app_with_source_event_sender(); + let asset_server = app.world().resource::().clone(); + + dir.insert_asset_text(Path::new("abc.rsp"), ""); + + #[derive(Asset, TypePath)] + pub struct TestAsset(Handle); + struct DeferredSelfLoader; + + impl AssetLoader for DeferredSelfLoader { + type Asset = TestAsset; + type Error = crate::loader::LoadDirectError; + type Settings = (); + + async fn load( + &self, + _: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let asset_path = load_context.path().clone(); + let loaded_asset = load_context.load::(asset_path); + Ok(TestAsset(loaded_asset)) + } + + fn extensions(&self) -> &[&str] { + &["rsp"] + } + } + + app.init_asset::() + .register_asset_loader(DeferredSelfLoader); + + let handle: Handle = asset_server.load("abc.rsp"); + + run_app_until(&mut app, |world| match asset_server.load_state(&handle) { + LoadState::Loading => None, + LoadState::Loaded => { + let test_assets = world.resource::>(); + let asset = test_assets.get(&handle).unwrap(); + assert_eq!(handle, asset.0); + Some(()) + } + state => panic!("Unexpected asset state: {state:?}"), + }); + + run_app_until(&mut app, |world| { + let messages = collect_asset_events(world); + if messages.is_empty() { + return None; + } + assert_eq!( + messages, + [ + AssetEvent::LoadedWithDependencies { id: handle.id() }, + AssetEvent::Added { id: handle.id() } + ] + ); + Some(()) + }); + + // Sending an asset event should result in the asset being reloaded - resulting in a + // "Modified" message. + source_events + .send_blocking(AssetSourceEvent::ModifiedAsset(PathBuf::from("abc.rsp"))) + .unwrap(); + + run_app_until(&mut app, |world| { + let messages = collect_asset_events(world); + if messages.is_empty() { + return None; + } + assert_eq!( + messages, + [ + AssetEvent::LoadedWithDependencies { id: handle.id() }, + AssetEvent::Modified { id: handle.id() } + ] + ); + Some(()) + }); + } + + #[test] + fn no_error_on_read_bytes_of_self_path() { + let (mut app, dir, source_events) = create_app_with_source_event_sender(); + let asset_server = app.world().resource::().clone(); + + dir.insert_asset_text(Path::new("abc.rsp"), ""); + + struct ReadBytesSelfLoader; + + impl AssetLoader for ReadBytesSelfLoader { + type Asset = TestAsset; + type Error = crate::loader::LoadDirectError; + type Settings = (); + + async fn load( + &self, + _: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let asset_path = load_context.path().clone(); + let _bytes = load_context.read_asset_bytes(asset_path).await.unwrap(); + Ok(TestAsset) + } + + fn extensions(&self) -> &[&str] { + &["rsp"] + } + } + + app.init_asset::() + .register_asset_loader(ReadBytesSelfLoader); + + let handle: Handle = asset_server.load("abc.rsp"); + + run_app_until(&mut app, |_world| match asset_server.load_state(&handle) { + LoadState::Loading => None, + LoadState::Loaded => Some(()), + state => panic!("Unexpected asset state: {state:?}"), + }); + + run_app_until(&mut app, |world| { + let messages = collect_asset_events(world); + if messages.is_empty() { + return None; + } + assert_eq!( + messages, + [ + AssetEvent::LoadedWithDependencies { id: handle.id() }, + AssetEvent::Added { id: handle.id() } + ] + ); + Some(()) + }); + + // Sending an asset event should result in the asset being reloaded - resulting in a + // "Modified" message. + source_events + .send_blocking(AssetSourceEvent::ModifiedAsset(PathBuf::from("abc.rsp"))) + .unwrap(); + + run_app_until(&mut app, |world| { + let messages = collect_asset_events(world); + if messages.is_empty() { + return None; + } + assert_eq!( + messages, + [ + AssetEvent::LoadedWithDependencies { id: handle.id() }, + AssetEvent::Modified { id: handle.id() } + ] + ); + Some(()) + }); + } + + /// This is not a statement of intent but of behavior: One may load + /// their self path deferred of unknown type without error. It has the same + /// asset index as the original handle, but not the same type. And it can + /// reload. + #[test] + fn no_error_on_unknown_type_deferred_load_of_self_path() { + let (mut app, dir, source_events) = create_app_with_source_event_sender(); + let asset_server = app.world().resource::().clone(); + + dir.insert_asset_text(Path::new("abc.rsp"), ""); + + #[derive(Asset, TypePath)] + pub struct TestAssetUD(Handle); + struct ImmediateSelfLoader; + + impl AssetLoader for ImmediateSelfLoader { + type Asset = TestAssetUD; + type Error = crate::loader::LoadDirectError; + type Settings = (); + + async fn load( + &self, + _: &mut dyn Reader, + _: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let asset_path = load_context.path().clone(); + let untyped_handle: Handle = + load_context.loader().with_unknown_type().load(asset_path); + + Ok(TestAssetUD(untyped_handle)) + } + + fn extensions(&self) -> &[&str] { + &["rsp"] + } + } + + app.init_asset::() + .register_asset_loader(ImmediateSelfLoader); + + let handle: Handle = asset_server.load("abc.rsp"); + + run_app_until(&mut app, |world| match asset_server.load_state(&handle) { + LoadState::Loading => None, + LoadState::Loaded => { + let asset_id = { + let test_assets = world.resource::>(); + let asset = test_assets.get(&handle).unwrap(); + asset.0.id() + }; + let untyped = world.resource::>(); + if let Some(untyped_handle) = untyped.get(asset_id) { + assert_eq!(handle.id(), untyped_handle.handle.id()); + Some(()) + } else { + None + } + } + state => panic!("Unexpected asset state: {state:?}"), + }); + + run_app_until(&mut app, |world| { + let messages = collect_asset_events(world); + if messages.is_empty() { + return None; + } + assert_eq!( + messages, + [ + AssetEvent::LoadedWithDependencies { id: handle.id() }, + AssetEvent::Added { id: handle.id() } + ] + ); + Some(()) + }); + + // Sending an asset event should result in the asset being reloaded - resulting in a + // "Modified" message. + source_events + .send_blocking(AssetSourceEvent::ModifiedAsset(PathBuf::from("abc.rsp"))) + .unwrap(); + + run_app_until(&mut app, |world| { + let messages = collect_asset_events(world); + if messages.is_empty() { + return None; + } + assert_eq!( + messages, + [ + AssetEvent::LoadedWithDependencies { id: handle.id() }, + AssetEvent::Modified { id: handle.id() } + ] + ); + Some(()) + }); + } + // validate the Asset derive macro for various asset types #[derive(Asset, TypePath)] pub struct TestAsset; diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index cf40e8840ad50..3b0a2cfd7dc91 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -305,6 +305,8 @@ pub enum LoadDirectError { dependency: AssetPath<'static>, error: AssetLoadError, }, + #[error("The asset at path `{0:?}` requested to immediately load itself recursively, but this is not supported")] + RequestedSelfPath(AssetPath<'static>), } /// An error that occurs while deserializing [`AssetMeta`]. @@ -507,7 +509,9 @@ impl<'a> LoadContext<'a> { path: path.path().to_path_buf(), source, })?; - self.loader_dependencies.insert(path.clone_owned(), hash); + if self.asset_path != path { + self.loader_dependencies.insert(path.clone_owned(), hash); + } Ok(bytes) } @@ -533,6 +537,11 @@ impl<'a> LoadContext<'a> { loader: &dyn ErasedAssetLoader, reader: &mut dyn Reader, ) -> Result { + if self.asset_path == path { + return Err(LoadDirectError::RequestedSelfPath( + self.asset_path.clone_owned(), + )); + } let loaded_asset = self .asset_server .load_with_meta_loader_and_reader( diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 994eb33590bee..77a5702d82f98 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -9,6 +9,7 @@ use crate::{ }; use alloc::{borrow::ToOwned, boxed::Box, sync::Arc}; use core::any::TypeId; +use tracing::debug; // Utility type for handling the sources of reader references enum ReaderRef<'a> { @@ -304,6 +305,8 @@ impl NestedLoader<'_, '_, StaticTyped, Deferred> { /// [`with_unknown_type`]: Self::with_unknown_type pub fn load<'c, A: Asset>(self, path: impl Into>) -> Handle { let path = path.into().to_owned(); + + let is_self_path = *self.load_context.path() == path; let handle = if self.load_context.should_load_dependencies { self.load_context.asset_server.load_with_meta_transform( path, @@ -318,8 +321,15 @@ impl NestedLoader<'_, '_, StaticTyped, Deferred> { }; // `load_with_meta_transform` and `get_or_create_path_handle` always returns a Strong // variant, so we are safe to unwrap. - let index = (&handle).try_into().unwrap(); - self.load_context.dependencies.insert(index); + if !is_self_path { + let index = (&handle).try_into().unwrap(); + self.load_context.dependencies.insert(index); + } else { + debug!( + "Asset from path `{:?}` loaded its self path", + self.load_context.path() + ); + } handle } } @@ -367,6 +377,7 @@ impl NestedLoader<'_, '_, UnknownTyped, Deferred> { /// This will infer the asset type from metadata. pub fn load<'p>(self, path: impl Into>) -> Handle { let path = path.into().to_owned(); + let is_self_path = *self.load_context.path() == path; let handle = if self.load_context.should_load_dependencies { self.load_context .asset_server @@ -379,7 +390,15 @@ impl NestedLoader<'_, '_, UnknownTyped, Deferred> { // `load_unknown_type_with_meta_transform` and `get_or_create_path_handle` always returns a // Strong variant, so we are safe to unwrap. let index = (&handle).try_into().unwrap(); - self.load_context.dependencies.insert(index); + + if !is_self_path { + self.load_context.dependencies.insert(index); + } else { + debug!( + "Asset from path `{:?}` of unknown type loaded its self path", + self.load_context.path() + ); + } handle } } @@ -402,6 +421,9 @@ impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> if path.label().is_some() { return Err(LoadDirectError::RequestedSubasset(path.clone())); } + if self.load_context.path() == path { + return Err(LoadDirectError::RequestedSelfPath(path.clone())); + } self.load_context .asset_server .write_infos() diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 9f7d594e5d3bb..d08ca3a6518e6 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -1788,6 +1788,13 @@ pub fn handle_internal_asset_events(world: &mut World) { ) { if let Some(dependents) = infos.loader_dependents.get(asset_path) { for dependent in dependents { + assert_ne!( + asset_path, dependent, + "The asset path `{}` contains itself as a dependent.", + &asset_path + ); + // If the above assertion fails, the following code would + // cause a stackoverflow. paths_to_reload.insert(dependent.to_owned()); queue_ancestors(dependent, infos, paths_to_reload); } From 18c2b568c666f20d993f5e4073c80aae5db46b39 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Sun, 14 Dec 2025 16:26:51 -0800 Subject: [PATCH 112/133] `spec_v2`: migrate TAA (#22070) # Objective - Migrate TAA to `spec_v2` ## Testing - Ran `anti_aliasing` example, seems fine --- crates/bevy_anti_alias/src/taa/mod.rs | 104 ++++++++++++++------------ 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/crates/bevy_anti_alias/src/taa/mod.rs b/crates/bevy_anti_alias/src/taa/mod.rs index e396e9accd67b..377de519c1560 100644 --- a/crates/bevy_anti_alias/src/taa/mod.rs +++ b/crates/bevy_anti_alias/src/taa/mod.rs @@ -1,5 +1,5 @@ use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; use bevy_camera::{Camera, Camera3d}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, @@ -8,6 +8,7 @@ use bevy_core_pipeline::{ }; use bevy_diagnostic::FrameCount; use bevy_ecs::{ + error::BevyError, prelude::{Component, Entity, ReflectComponent}, query::{QueryItem, With}, resource::Resource, @@ -25,11 +26,11 @@ use bevy_render::{ render_resource::{ binding_types::{sampler, texture_2d, texture_depth_2d}, BindGroupEntries, BindGroupLayoutDescriptor, BindGroupLayoutEntries, - CachedRenderPipelineId, ColorTargetState, ColorWrites, FilterMode, FragmentState, - Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, - RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, ShaderStages, - SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor, TextureDimension, - TextureFormat, TextureSampleType, TextureUsages, + CachedRenderPipelineId, Canonical, ColorTargetState, ColorWrites, FilterMode, + FragmentState, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, + RenderPipeline, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, + ShaderStages, Specializer, SpecializerKey, TextureDescriptor, TextureDimension, + TextureFormat, TextureSampleType, TextureUsages, Variants, }, renderer::{RenderContext, RenderDevice}, sync_component::SyncComponentPlugin, @@ -38,7 +39,6 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget}, ExtractSchedule, MainWorld, Render, RenderApp, RenderStartup, RenderSystems, }; -use bevy_shader::Shader; use bevy_utils::default; use tracing::warn; @@ -58,7 +58,6 @@ impl Plugin for TemporalAntiAliasPlugin { return; }; render_app - .init_resource::>() .add_systems(RenderStartup, init_taa_pipeline) .add_systems(ExtractSchedule, extract_taa_settings) .add_systems( @@ -239,8 +238,7 @@ struct TaaPipeline { taa_bind_group_layout: BindGroupLayoutDescriptor, nearest_sampler: Sampler, linear_sampler: Sampler, - fullscreen_shader: FullscreenShader, - fragment_shader: Handle, + variants: Variants, } fn init_taa_pipeline( @@ -283,61 +281,68 @@ fn init_taa_pipeline( ), ); + let fragment_shader = load_embedded_asset!(asset_server.as_ref(), "taa.wgsl"); + + let variants = Variants::new( + TaaPipelineSpecializer, + RenderPipelineDescriptor { + label: Some("taa_pipeline".into()), + layout: vec![taa_bind_group_layout.clone()], + vertex: fullscreen_shader.to_vertex_state(), + fragment: Some(FragmentState { + shader: fragment_shader, + ..default() + }), + ..default() + }, + ); + commands.insert_resource(TaaPipeline { taa_bind_group_layout, nearest_sampler, linear_sampler, - fullscreen_shader: fullscreen_shader.clone(), - fragment_shader: load_embedded_asset!(asset_server.as_ref(), "taa.wgsl"), + variants, }); } -#[derive(PartialEq, Eq, Hash, Clone)] +struct TaaPipelineSpecializer; + +#[derive(PartialEq, Eq, Hash, Clone, SpecializerKey)] struct TaaPipelineKey { hdr: bool, reset: bool, } -impl SpecializedRenderPipeline for TaaPipeline { +impl Specializer for TaaPipelineSpecializer { type Key = TaaPipelineKey; - fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { - let mut shader_defs = vec![]; - + fn specialize( + &self, + key: Self::Key, + descriptor: &mut RenderPipelineDescriptor, + ) -> Result, BevyError> { + let fragment = descriptor.fragment_mut()?; let format = if key.hdr { - shader_defs.push("TONEMAP".into()); + fragment.shader_defs.push("TONEMAP".into()); ViewTarget::TEXTURE_FORMAT_HDR } else { TextureFormat::bevy_default() }; if key.reset { - shader_defs.push("RESET".into()); + fragment.shader_defs.push("RESET".into()); } - RenderPipelineDescriptor { - label: Some("taa_pipeline".into()), - layout: vec![self.taa_bind_group_layout.clone()], - vertex: self.fullscreen_shader.to_vertex_state(), - fragment: Some(FragmentState { - shader: self.fragment_shader.clone(), - shader_defs, - targets: vec![ - Some(ColorTargetState { - format, - blend: None, - write_mask: ColorWrites::ALL, - }), - Some(ColorTargetState { - format, - blend: None, - write_mask: ColorWrites::ALL, - }), - ], - ..default() - }), - ..default() - } + let color_target_state = ColorTargetState { + format, + blend: None, + write_mask: ColorWrites::ALL, + }; + + fragment.set_target(0, color_target_state.clone()); + fragment.set_target(1, color_target_state); + + Ok(key) } } @@ -455,25 +460,30 @@ pub struct TemporalAntiAliasPipelineId(CachedRenderPipelineId); fn prepare_taa_pipelines( mut commands: Commands, pipeline_cache: Res, - mut pipelines: ResMut>, - pipeline: Res, + mut pipeline: ResMut, views: Query<(Entity, &ExtractedView, &TemporalAntiAliasing)>, -) { +) -> Result<(), BevyError> { for (entity, view, taa_settings) in &views { let mut pipeline_key = TaaPipelineKey { hdr: view.hdr, reset: taa_settings.reset, }; - let pipeline_id = pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key.clone()); + let pipeline_id = pipeline + .variants + .specialize(&pipeline_cache, pipeline_key.clone())?; // Prepare non-reset pipeline anyways - it will be necessary next frame if pipeline_key.reset { pipeline_key.reset = false; - pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key); + pipeline + .variants + .specialize(&pipeline_cache, pipeline_key)?; } commands .entity(entity) .insert(TemporalAntiAliasPipelineId(pipeline_id)); } + + Ok(()) } From 48efe26c6809881158b2b544d9852d79cbf43dc0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 15 Dec 2025 00:31:23 +0000 Subject: [PATCH 113/133] UI scrollbars clipping fix (#21910) # Objective #### Fix two problems 1. Scrollbars are placed in the padding region of the node, not the content region. The content insets returned by `ComputedNode::content_inset` should account for the sizes of the scrollbars. 3. In `update_clipping_system` the clipping rect calculations are incorrect, `scrollbar_size` should only be added to the `right` and `bottom` when `visual_area` is set to `ContentBox`. For `BorderBox` and `PaddingBox`, the scrollbars are inside the clipping rect. ## Solution 1. Add the scrollbar size to the `right` and `bottom` insets returned by `ComputedNode::content_inset`. 2. Since `content_inset` now includes `scrollbar size`, remove the subtraction from `clip_rect.max` in `update_clipping_system`. ## Testing Look at: ``` cargo run --example scrollbars --features="experimental_bevy_ui_widgets" ``` --- crates/bevy_ui/src/ui_node.rs | 17 ++++++++++------- crates/bevy_ui/src/update.rs | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index e4b18e6f53c22..7d114ea72d802 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -222,7 +222,10 @@ impl ComputedNode { /// Returns the combined inset on each edge including both padding and border thickness in physical pixels. #[inline] pub fn content_inset(&self) -> BorderRect { - self.border + self.padding + let mut content_inset = self.border + self.padding; + content_inset.right += self.scrollbar_size.x; + content_inset.bottom += self.scrollbar_size.y; + content_inset } /// Returns the inverse of the scale factor for this node. @@ -322,9 +325,9 @@ impl ComputedNode { let content_inset = self.content_inset(); let half_size = 0.5 * self.size; let min_x = -half_size.x + content_inset.left; - let max_x = half_size.x - content_inset.right - self.scrollbar_size.x; - let max_y = half_size.y - content_inset.bottom; - let min_y = max_y - self.scrollbar_size.y; + let max_x = half_size.x - content_inset.right; + let min_y = half_size.y - content_inset.bottom; + let max_y = min_y + self.scrollbar_size.y; let gutter = Rect { min: Vec2::new(min_x, min_y), max: Vec2::new(max_x, max_y), @@ -348,10 +351,10 @@ impl ComputedNode { } let content_inset = self.content_inset(); let half_size = 0.5 * self.size; - let max_x = half_size.x - content_inset.right; - let min_x = max_x - self.scrollbar_size.x; + let min_x = half_size.x - content_inset.right; + let max_x = min_x + self.scrollbar_size.x; let min_y = -half_size.y + content_inset.top; - let max_y = half_size.y - content_inset.bottom - self.scrollbar_size.y; + let max_y = half_size.y - content_inset.bottom; let gutter = Rect { min: Vec2::new(min_x, min_y), max: Vec2::new(max_x, max_y), diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index e1e789577c53f..ff2317e9fecdd 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -111,8 +111,8 @@ fn update_clipping( clip_rect.min.x += clip_inset.left; clip_rect.min.y += clip_inset.top; - clip_rect.max.x -= clip_inset.right + computed_node.scrollbar_size.x; - clip_rect.max.y -= clip_inset.bottom + computed_node.scrollbar_size.y; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; clip_rect = clip_rect .inflate(node.overflow_clip_margin.margin.max(0.) / computed_node.inverse_scale_factor); From 4eed4d042727a139d8d6513790c4bf5b161103f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Mon, 15 Dec 2025 01:39:25 +0100 Subject: [PATCH 114/133] Easy screenrecording plugin (#21237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Followup on #21235 - See https://github.com/mockersf/bevy/compare/easy-screenshots...mockersf:bevy:easy-screenrecording for what's new - Be able to record videos from Bevy in a consistent manner ## Solution - Make a new `EasyScreenRecordPlugin` in the dev tools ## Testing - Add to any example ``` .add_plugins(bevy::dev_tools::EasyScreenRecordPlugin::default()) ``` - Run the example with the feature `bevy_internal/screenrecording` enabled - press the space bar - wait for it... - press the space bar again - screen recording! 🎉 - almost... you now have a h264 file. VLC can read them, but they are not the most friendly format - `ffmpeg` is our friend! `for file in *.h264; do ffmpeg -i $file $file.mp4; done` - you now have a .mp4 file that can be shared anywhere! --- ## Showcase directly taken by Bevy https://github.com/user-attachments/assets/217f5093-9443-40e5-b2ce-33f65f6a56c6 --- .github/actions/install-linux-deps/action.yml | 5 + .github/workflows/ci.yml | 2 + crates/bevy_dev_tools/Cargo.toml | 3 + crates/bevy_dev_tools/src/easy_screenshot.rs | 192 ++++++++++++++++++ crates/bevy_internal/Cargo.toml | 2 + .../release-notes/easy_marketing_material.md | 9 + 6 files changed, 213 insertions(+) create mode 100644 release-content/release-notes/easy_marketing_material.md diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index c494200e5a649..401eaa76d2562 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -33,6 +33,10 @@ inputs: description: Install xkb (libxkbcommon-dev) required: false default: "false" + x264: + description: Install x264 (libx264-dev) + required: false + default: "false" runs: using: composite steps: @@ -47,3 +51,4 @@ runs: ${{ fromJSON(inputs.udev) && 'libudev-dev' || '' }} ${{ fromJSON(inputs.wayland) && 'libwayland-dev' || '' }} ${{ fromJSON(inputs.xkb) && 'libxkbcommon-dev' || '' }} + ${{ fromJSON(inputs.x264) && 'libx264-164 libx264-dev' || '' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b687b032e3d13..ed7694c281301 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,7 @@ jobs: with: wayland: true xkb: true + x264: true - name: CI job # See tools/ci/src/main.rs for the commands this runs run: cargo run -p ci -- lints @@ -371,6 +372,7 @@ jobs: with: wayland: true xkb: true + x264: true - name: Build and check doc # See tools/ci/src/main.rs for the commands this runs run: cargo run -p ci -- doc diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index dc218f023baef..36854b3f5088e 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [features] bevy_ci_testing = ["serde", "ron"] +screenrecording = ["x264"] webgl = ["bevy_render/webgl"] webgpu = ["bevy_render/webgpu"] @@ -21,6 +22,7 @@ bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" } bevy_color = { path = "../bevy_color", version = "0.18.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.18.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.18.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.18.0-dev" } bevy_input = { path = "../bevy_input", version = "0.18.0-dev" } bevy_math = { path = "../bevy_math", version = "0.18.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.18.0-dev" } @@ -38,6 +40,7 @@ bevy_state = { path = "../bevy_state", version = "0.18.0-dev" } serde = { version = "1.0", features = ["derive"], optional = true } ron = { version = "0.12", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } +x264 = { version = "0.5.0", optional = true } [lints] workspace = true diff --git a/crates/bevy_dev_tools/src/easy_screenshot.rs b/crates/bevy_dev_tools/src/easy_screenshot.rs index 4bfacf995f261..4b5933038313e 100644 --- a/crates/bevy_dev_tools/src/easy_screenshot.rs +++ b/crates/bevy_dev_tools/src/easy_screenshot.rs @@ -1,10 +1,26 @@ +#[cfg(feature = "screenrecording")] +use core::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; +#[cfg(feature = "screenrecording")] +use std::{fs::File, io::Write, sync::mpsc::channel}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; +#[cfg(feature = "screenrecording")] +use bevy_image::Image; use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode}; +#[cfg(feature = "screenrecording")] +use bevy_render::view::screenshot::ScreenshotCaptured; use bevy_render::view::screenshot::{save_to_disk, Screenshot}; +#[cfg(feature = "screenrecording")] +use bevy_time::Time; use bevy_window::{PrimaryWindow, Window}; +#[cfg(feature = "screenrecording")] +use tracing::info; +#[cfg(feature = "screenrecording")] +use x264::{Colorspace, Encoder, Setup}; +#[cfg(feature = "screenrecording")] +pub use x264::{Preset, Tune}; /// File format the screenshot will be saved in #[derive(Clone, Copy)] @@ -65,3 +81,179 @@ impl Plugin for EasyScreenshotPlugin { ); } } + +#[cfg(feature = "screenrecording")] +/// Add this plugin to your app to enable easy screen recording. +pub struct EasyScreenRecordPlugin { + /// The key to toggle recording. + pub toggle: KeyCode, + /// h264 encoder preset + pub preset: Preset, + /// h264 encoder tune + pub tune: Tune, + /// target frame time + pub frame_time: Duration, +} + +#[cfg(feature = "screenrecording")] +impl Default for EasyScreenRecordPlugin { + fn default() -> Self { + EasyScreenRecordPlugin { + toggle: KeyCode::Space, + preset: Preset::Medium, + tune: Tune::Animation, + frame_time: Duration::from_millis(33), + } + } +} + +#[cfg(feature = "screenrecording")] +enum RecordCommand { + Start(String, Preset, Tune), + Stop, + Frame(Image), +} + +#[cfg(feature = "screenrecording")] +/// Controls screen recording +#[derive(Message)] +pub enum RecordScreen { + /// Starts screen recording + Start, + /// Stops screen recording + Stop, +} + +#[cfg(feature = "screenrecording")] +impl Plugin for EasyScreenRecordPlugin { + fn build(&self, app: &mut App) { + let (tx, rx) = channel::(); + + let frame_time = self.frame_time; + + std::thread::spawn(move || { + let mut encoder: Option = None; + let mut setup = None; + let mut file: Option = None; + let mut frame = 0; + loop { + let Ok(next) = rx.recv() else { + break; + }; + match next { + RecordCommand::Start(name, preset, tune) => { + info!("starting recording at {}", name); + file = Some(File::create(name).unwrap()); + setup = Some(Setup::preset(preset, tune, false, true).high()); + } + RecordCommand::Stop => { + info!("stopping recording"); + if let Some(encoder) = encoder.take() { + let mut flush = encoder.flush(); + let mut file = file.take().unwrap(); + while let Some(result) = flush.next() { + let (data, _) = result.unwrap(); + file.write_all(data.entirety()).unwrap(); + } + } + } + RecordCommand::Frame(image) => { + if let Some(setup) = setup.take() { + let mut new_encoder = setup + .fps((1000 / frame_time.as_millis()) as u32, 1) + .build(Colorspace::RGB, image.width() as i32, image.height() as i32) + .unwrap(); + let headers = new_encoder.headers().unwrap(); + file.as_mut() + .unwrap() + .write_all(headers.entirety()) + .unwrap(); + encoder = Some(new_encoder); + } + if let Some(encoder) = encoder.as_mut() { + let pts = (frame_time.as_millis() * frame) as i64; + + frame += 1; + let (data, _) = encoder + .encode( + pts, + x264::Image::rgb( + image.width() as i32, + image.height() as i32, + &image.try_into_dynamic().unwrap().to_rgb8(), + ), + ) + .unwrap(); + file.as_mut().unwrap().write_all(data.entirety()).unwrap(); + } + } + } + } + }); + + let frame_time = self.frame_time; + + app.add_message::().add_systems( + Update, + ( + (move |mut messages: MessageWriter, mut recording: Local| { + *recording = !*recording; + if *recording { + messages.write(RecordScreen::Start); + } else { + messages.write(RecordScreen::Stop); + } + }) + .run_if(input_just_pressed(self.toggle)), + { + let tx = tx.clone(); + let preset = self.preset; + let tune = self.tune; + move |mut commands: Commands, + mut recording: Local, + mut messages: MessageReader, + window: Single<&Window, With>, + current_screenshot: Query<(), With>, + mut virtual_time: ResMut>| { + match messages.read().last() { + Some(RecordScreen::Start) => { + let since_the_epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should go forward"); + let filename = format!( + "{}-{}.h264", + window.title, + since_the_epoch.as_millis(), + ); + tx.send(RecordCommand::Start(filename, preset, tune)) + .unwrap(); + *recording = true; + virtual_time.pause(); + } + Some(RecordScreen::Stop) => { + tx.send(RecordCommand::Stop).unwrap(); + *recording = false; + virtual_time.unpause(); + } + _ => {} + } + if *recording && current_screenshot.single().is_err() { + let tx = tx.clone(); + commands.spawn(Screenshot::primary_window()).observe( + move |screenshot_captured: On, + mut virtual_time: ResMut>, + mut time: ResMut>| { + let img = screenshot_captured.image.clone(); + tx.send(RecordCommand::Frame(img)).unwrap(); + virtual_time.advance_by(frame_time); + *time = virtual_time.as_generic(); + }, + ); + } + } + }, + ) + .chain(), + ); + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c894e3664405f..7aac25bb4b432 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -430,6 +430,8 @@ hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] debug = ["bevy_utils/debug", "bevy_ecs/debug"] +screenrecording = ["bevy_dev_tools/screenrecording"] + [dependencies] # bevy (no_std) bevy_app = { path = "../bevy_app", version = "0.18.0-dev", default-features = false, features = [ diff --git a/release-content/release-notes/easy_marketing_material.md b/release-content/release-notes/easy_marketing_material.md new file mode 100644 index 0000000000000..953db7e9dcaf2 --- /dev/null +++ b/release-content/release-notes/easy_marketing_material.md @@ -0,0 +1,9 @@ +--- +title: Helpers to Produce Marketing Material +authors: ["@mockersf"] +pull_requests: [21235, 21237] +--- + +Bevy can take a screenshot of what's rendered since 0.11. This is now easier to setup to help you create marketing material, so that you can take screenshot with consistent formatting with the new `EasyScreenshotPlugin`. With its default settings, once you add this plugin to your application, a PNG screenshot will be taken when you press the `PrintScreen` key. You can change the trigger key, or the screenshot format between PNG, JPEG or BMP. + +It is now possible to record a movie from Bevy, with the new `EasyScreenRecordPlugin`. This plugins add a toggle key, space bar by default, that will toggle screen recording. Recording can also be started and stopped programmatically with the `RecordScreen` messages. From 9abb6238c1778950c85aa69c7566e513ae591142 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 15 Dec 2025 01:48:56 +0000 Subject: [PATCH 115/133] single query per node in UI picking backend (#22049) # Objective The UI picking backend queries twice for the node data. Simplify `ui_picking` by storing the camera entity and picking data in `hit_nodes` instead of the second query. ## Solution Store the camera entity and picking data in `hit_nodes` instead of querying for them again. --- crates/bevy_ui/src/picking_backend.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 1028cfb6b0e5a..d3648ced54f90 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -149,7 +149,8 @@ pub fn ui_picking( } // The list of node entities hovered for each (camera, pointer) combo - let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<(Entity, Entity, Vec2)>>::default(); + let mut hit_nodes = + HashMap::<(Entity, PointerId), Vec<(Entity, Entity, Option, Vec2)>>::default(); // prepare an iterator that contains all the nodes that have the cursor in their rect, // from the top node to the bottom one. this will also reset the interaction to `None` @@ -222,6 +223,7 @@ pub fn ui_picking( .push(( text_entity, camera_entity, + node.pickable.cloned(), node.transform.inverse().transform_point2(*cursor_position) / node.node.size(), )); @@ -240,6 +242,7 @@ pub fn ui_picking( .push(( node_entity, camera_entity, + node.pickable.cloned(), node.transform.inverse().transform_point2(*cursor_position) / node.node.size(), )); @@ -254,13 +257,13 @@ pub fn ui_picking( let mut picks = Vec::new(); let mut depth = 0.0; - for (hovered_node, camera_entity, position) in hovered { + for (hovered_node, camera_entity, pickable, position) in hovered { picks.push(( *hovered_node, HitData::new(*camera_entity, depth, Some(position.extend(0.0)), None), )); - if let Ok(pickable) = pickable_query.get(*hovered_node) { + if let Some(pickable) = pickable { // If an entity has a `Pickable` component, we will use that as the source of truth. if pickable.should_block_lower { break; From c18838fd3574ba8a89e16bcd88d94aad0e8226bb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 15 Dec 2025 01:58:40 +0000 Subject: [PATCH 116/133] `ComputedNode` box model helper functions (#21903) # Objective Add helper functions to `ComputedNode` that return the bounds of the node's border, padding, and content areas. ## Solution Add helper functions `border_box`, `padding_box`, and `content_box` to `ComputedNode`. # Included a release note. These changes are very trivial and don't really need one, but I've got quite a few more less trivial changes to `ComputedNode` that I want to add later. ## Testing This PR also includes a couple of trivial tests. --- crates/bevy_ui/src/ui_node.rs | 85 ++++++++++++++++++- .../computed_node_helper_functions.md | 7 ++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 release-content/release-notes/computed_node_helper_functions.md diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 7d114ea72d802..7ab5cd97a9bfc 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -302,6 +302,38 @@ impl ComputedNode { clip_rect } + /// Returns the node's border-box in object-centered physical coordinates. + /// This is the full rectangle enclosing the node. + #[inline] + pub fn border_box(&self) -> Rect { + Rect::from_center_size(Vec2::ZERO, self.size) + } + + /// Returns the node's padding-box in object-centered physical coordinates. + /// This is the region inside the border containing the node's padding and content areas. + #[inline] + pub fn padding_box(&self) -> Rect { + let mut out = self.border_box(); + out.min.x += self.border.left; + out.max.x -= self.border.right; + out.min.y += self.border.top; + out.max.y -= self.border.bottom; + out + } + + /// Returns the node's content-box in object-centered physical coordinates. + /// This is the innermost region of the node, where its content is placed. + #[inline] + pub fn content_box(&self) -> Rect { + let mut out = self.border_box(); + let content_inset = self.content_inset(); + out.min.x += content_inset.left; + out.max.x -= content_inset.right; + out.min.y += content_inset.top; + out.max.y -= content_inset.bottom; + out + } + const fn compute_thumb( gutter_min: f32, content_length: f32, @@ -2987,11 +3019,10 @@ impl ComputedUiRenderTargetInfo { #[cfg(test)] mod tests { - use bevy_math::Rect; - use bevy_math::Vec2; - use crate::ComputedNode; use crate::GridPlacement; + use bevy_math::{Rect, Vec2}; + use bevy_sprite::BorderRect; #[test] fn invalid_grid_placement_values() { @@ -3116,4 +3147,52 @@ mod tests { ); assert_eq!(thumb, [0., 50.]); } + + #[test] + fn border_box_is_centered_rect_of_node_size() { + let node = ComputedNode { + size: Vec2::new(100.0, 50.0), + ..Default::default() + }; + let border_box = node.border_box(); + + assert_eq!(border_box.min, Vec2::new(-50.0, -25.0)); + assert_eq!(border_box.max, Vec2::new(50.0, 25.0)); + } + + #[test] + fn padding_box_subtracts_border_thickness() { + let node = ComputedNode { + size: Vec2::new(100.0, 60.0), + border: BorderRect { + left: 5.0, + right: 7.0, + top: 3.0, + bottom: 9.0, + }, + ..Default::default() + }; + let padding_box = node.padding_box(); + + assert_eq!(padding_box.min, Vec2::new(-50.0 + 5.0, -30.0 + 3.0)); + assert_eq!(padding_box.max, Vec2::new(50.0 - 7.0, 30.0 - 9.0)); + } + + #[test] + fn content_box_uses_content_inset() { + let node = ComputedNode { + size: Vec2::new(80.0, 40.0), + padding: BorderRect { + left: 4.0, + right: 6.0, + top: 2.0, + bottom: 8.0, + }, + ..Default::default() + }; + let content_box = node.content_box(); + + assert_eq!(content_box.min, Vec2::new(-40.0 + 4.0, -20.0 + 2.0)); + assert_eq!(content_box.max, Vec2::new(40.0 - 6.0, 20.0 - 8.0)); + } } diff --git a/release-content/release-notes/computed_node_helper_functions.md b/release-content/release-notes/computed_node_helper_functions.md new file mode 100644 index 0000000000000..b9a08ab8440ca --- /dev/null +++ b/release-content/release-notes/computed_node_helper_functions.md @@ -0,0 +1,7 @@ +--- +title: "`ComputedNode` helper functions" +authors: ["@ickshonpe"] +pull_requests: [21903] +--- + +Helper functions `border_box`, `padding_box`, and `content_box` that return a node’s object-centered border, padding, and content boxes have been added to `ComputedNode`. From ec072d8f117763f214a6fe56f4b1ecf4af90c0d4 Mon Sep 17 00:00:00 2001 From: Chris Biscardi Date: Sun, 14 Dec 2025 18:03:18 -0800 Subject: [PATCH 117/133] reflect rendertarget (#22113) RenderTarget was a Component that derived Reflect but did not Reflect Component as of this merge today: https://github.com/bevyengine/bevy/commit/67633b34da677a825ea3c6900b884d012d56999b Without this fix, spawning a scene with a camera (such as from a Blender export) causes a panic: > scene contains the unregistered component `bevy_camera::camera::RenderTarget`. consider adding `#[reflect(Component)]` to your type --- crates/bevy_camera/src/camera.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index 073e6a06b2f35..f83834aef5cc4 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -811,7 +811,7 @@ impl Default for CameraOutputMode { /// The "target" that a [`Camera`] will render to. For example, this could be a `Window` /// swapchain or an [`Image`]. #[derive(Component, Debug, Clone, Reflect, From)] -#[reflect(Clone)] +#[reflect(Clone, Component)] pub enum RenderTarget { /// Window to which the camera's view is rendered. Window(WindowRef), From 6ee5067857fe8f83e0cfa77130c1db06bb0115a8 Mon Sep 17 00:00:00 2001 From: Federico Rinaldi Date: Mon, 15 Dec 2025 03:14:04 +0100 Subject: [PATCH 118/133] Make BRP builtins utilities `parse` and `parse_some` public (#22005) # Objective Builtin BRP methods make constant use of `parse` and `parse_some` in parsing utilities. Making them public allows users to use them in their own code. ## Solution Make both functions `pub`. ## Testing I think testing is not necessary, especially since doc strings do not link anywhere. --- crates/bevy_remote/src/builtin_methods.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 4ee7f336aefc2..3c4c97d1388d3 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -479,7 +479,7 @@ pub struct BrpQueryRow { } /// A helper function used to parse a `serde_json::Value`. -fn parse Deserialize<'de>>(value: Value) -> Result { +pub fn parse Deserialize<'de>>(value: Value) -> Result { serde_json::from_value(value).map_err(|err| BrpError { code: error_codes::INVALID_PARAMS, message: err.to_string(), @@ -488,7 +488,7 @@ fn parse Deserialize<'de>>(value: Value) -> Result { } /// A helper function used to parse a `serde_json::Value` wrapped in an `Option`. -fn parse_some Deserialize<'de>>(value: Option) -> Result { +pub fn parse_some Deserialize<'de>>(value: Option) -> Result { match value { Some(value) => parse(value), None => Err(BrpError { From 030657a28bcb4f66fd6e3f09c2bb4c34736a3c9b Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 14 Dec 2025 18:15:43 -0800 Subject: [PATCH 119/133] Doc trivial ecs unsafe (#22014) # Objective - Trying to doc most of the unsafe in the ecs crate so we can turn on `unsafe_op_in_unsafe_fn`. ## Solution - Unfortunately reviewing the unsafe docs will probably not be trivial if we try to do it all in one pr. There are 400+ warnings when you turn on the lint. So we need to break it up as much as possible as reviewing the safety contracts in some cases isn't the easiest. - This pr includes two types of unsafe blocks. 1. Blocks that already had safety comments, but were missing the `unsafe {}` block. 2. Unsafe functions that have the same safety contract as their unsafe parent function and are very short. (Usually just the call to the function). --- crates/bevy_ecs/src/bundle/impls.rs | 16 ++++- crates/bevy_ecs/src/bundle/insert.rs | 16 +++-- crates/bevy_ecs/src/component/required.rs | 23 +++--- crates/bevy_ecs/src/entity/mod.rs | 2 +- crates/bevy_ecs/src/query/state.rs | 15 ++-- crates/bevy_ecs/src/schedule/executor/mod.rs | 6 +- crates/bevy_ecs/src/spawn.rs | 4 +- crates/bevy_ecs/src/storage/blob_array.rs | 3 +- crates/bevy_ecs/src/storage/mod.rs | 3 +- crates/bevy_ecs/src/storage/table/column.rs | 15 ++-- crates/bevy_ecs/src/system/combinator.rs | 13 ++-- crates/bevy_ecs/src/system/commands/mod.rs | 28 +++++--- crates/bevy_ecs/src/system/schedule_system.rs | 12 ++-- crates/bevy_ecs/src/system/system_param.rs | 70 ++++++++++++++----- .../world/entity_access/component_fetch.rs | 9 ++- .../src/world/entity_access/filtered.rs | 3 +- .../src/world/entity_access/world_mut.rs | 31 +++++--- crates/bevy_ecs/src/world/entity_fetch.rs | 9 ++- .../bevy_ecs/src/world/unsafe_world_cell.rs | 3 +- 19 files changed, 186 insertions(+), 95 deletions(-) diff --git a/crates/bevy_ecs/src/bundle/impls.rs b/crates/bevy_ecs/src/bundle/impls.rs index c8d9160dc2135..4083ae916b7d2 100644 --- a/crates/bevy_ecs/src/bundle/impls.rs +++ b/crates/bevy_ecs/src/bundle/impls.rs @@ -138,8 +138,14 @@ macro_rules! tuple_impl { bevy_ptr::deconstruct_moving_ptr!({ let tuple { $($index: $alias,)* } = ptr; }); + #[allow( + unused_unsafe, + reason = "Zero-length tuples will generate a function body equivalatent to (); however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] // SAFETY: Caller ensures requirements for calling `get_components` are met. - $( $name::get_components($alias, func); )* + unsafe { + $( $name::get_components($alias, func); )* + } } #[allow( @@ -151,8 +157,14 @@ macro_rules! tuple_impl { bevy_ptr::deconstruct_moving_ptr!({ let MaybeUninit:: { $($index: $alias,)* } = ptr; }); + #[allow( + unused_unsafe, + reason = "Zero-length tuples will generate a function body equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] // SAFETY: Caller ensures requirements for calling `apply_effect` are met. - $( $name::apply_effect($alias, entity); )* + unsafe { + $( $name::apply_effect($alias, entity); )* + } } } diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index cefdde59172fa..cc6d852d0f705 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -535,13 +535,15 @@ impl BundleInfo { }; }; // SAFETY: ids in self must be valid - let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert( - components, - observers, - table_id, - table_components, - sparse_set_components, - ); + let (new_archetype_id, is_new_created) = unsafe { + archetypes.get_id_or_insert( + components, + observers, + table_id, + table_components, + sparse_set_components, + ) + }; // Add an edge from the old archetype to the new archetype. archetypes[archetype_id] diff --git a/crates/bevy_ecs/src/component/required.rs b/crates/bevy_ecs/src/component/required.rs index 9974763623a2e..04abda66a9de6 100644 --- a/crates/bevy_ecs/src/component/required.rs +++ b/crates/bevy_ecs/src/component/required.rs @@ -390,11 +390,12 @@ impl Components { let old_required_count = required_components.all.len(); // SAFETY: the caller guarantees that `requiree` is valid in `self`. - self.required_components_scope(requiree, |this, required_components| { - // SAFETY: the caller guarantees that `required` is valid for type `R` in `self` - unsafe { required_components.register_by_id(required, this, constructor) }; - }); - + unsafe { + self.required_components_scope(requiree, |this, required_components| { + // SAFETY: the caller guarantees that `required` is valid for type `R` in `self` + required_components.register_by_id(required, this, constructor); + }); + } // Third step: update the required components and required_by of all the indirect requirements/requirees. // Borrow again otherwise it conflicts with the `self.required_components_scope` call. @@ -435,11 +436,13 @@ impl Components { // Skip the first one (requiree) because we already updates it. for &indirect_requiree in &new_requiree_components[1..] { // SAFETY: `indirect_requiree` comes from `self` so it must be valid. - self.required_components_scope(indirect_requiree, |this, required_components| { - // Rebuild the inherited required components. - // SAFETY: `required_components` comes from `self`, so all its components must have be valid in `self`. - unsafe { required_components.rebuild_inherited_required_components(this) }; - }); + unsafe { + self.required_components_scope(indirect_requiree, |this, required_components| { + // Rebuild the inherited required components. + // SAFETY: `required_components` comes from `self`, so all its components must have be valid in `self`. + required_components.rebuild_inherited_required_components(this); + }); + } } // Update the `required_by` of all the components that were newly required (directly or indirectly). diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 62c1c3de94eb7..db8425c260448 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -993,7 +993,7 @@ impl Entities { ) -> Option { self.ensure_index_index_is_valid(index); // SAFETY: We just did `ensure_index` - self.update_existing_location(index, location) + unsafe { self.update_existing_location(index, location) } } /// Ensures the index is within the bounds of [`Self::meta`], expanding it if necessary. diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 79ecf09b20176..448335db8ed0f 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1099,7 +1099,8 @@ impl QueryState { world: UnsafeWorldCell<'w>, entity: Entity, ) -> Result, QueryEntityError> { - self.query_unchecked(world).get_inner(entity) + // SAFETY: Upheld by caller + unsafe { self.query_unchecked(world) }.get_inner(entity) } /// Returns an [`Iterator`] over the query results for the given [`World`]. @@ -1314,7 +1315,8 @@ impl QueryState { &'s mut self, world: UnsafeWorldCell<'w>, ) -> QueryIter<'w, 's, D, F> { - self.query_unchecked(world).into_iter() + // SAFETY: Upheld by caller + unsafe { self.query_unchecked(world) }.into_iter() } /// Returns an [`Iterator`] over all possible combinations of `K` query results for the @@ -1333,7 +1335,8 @@ impl QueryState { &'s mut self, world: UnsafeWorldCell<'w>, ) -> QueryCombinationIter<'w, 's, D, F, K> { - self.query_unchecked(world).iter_combinations_inner() + // SAFETY: Upheld by caller + unsafe { self.query_unchecked(world) }.iter_combinations_inner() } /// Returns a parallel iterator over the query results for the given [`World`]. @@ -1755,7 +1758,8 @@ impl QueryState { &mut self, world: UnsafeWorldCell<'w>, ) -> Result, QuerySingleError> { - self.query_unchecked(world).single_inner() + // SAFETY: Upheld by caller + unsafe { self.query_unchecked(world) }.single_inner() } /// Returns a query result when there is exactly one entity matching the query, @@ -1780,8 +1784,7 @@ impl QueryState { // SAFETY: // - The caller ensured we have the correct access to the world. // - The caller ensured that the world matches. - self.query_unchecked_manual_with_ticks(world, last_run, this_run) - .single_inner() + unsafe { self.query_unchecked_manual_with_ticks(world, last_run, this_run) }.single_inner() } } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 197ba52be51c5..df297637a6037 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -260,7 +260,8 @@ mod __rust_begin_short_backtrace { system: &mut ScheduleSystem, world: UnsafeWorldCell, ) -> Result<(), RunSystemError> { - let result = system.run_unsafe((), world); + // SAFETY: Upheld by caller + let result = unsafe { system.run_unsafe((), world) }; // Call `black_box` to prevent this frame from being tail-call optimized away black_box(()); result @@ -276,7 +277,8 @@ mod __rust_begin_short_backtrace { world: UnsafeWorldCell, ) -> Result { // Call `black_box` to prevent this frame from being tail-call optimized away - black_box(system.run_unsafe((), world)) + // SAFETY: Upheld by caller + black_box(unsafe { system.run_unsafe((), world) }) } #[cfg(feature = "std")] diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index c01897870f953..c4224b99efc87 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -327,7 +327,7 @@ impl> DynamicBundle for SpawnRelatedBundle< // called exactly once for each component being fetched with the correct `StorageType` // - `Effect: !NoBundleEffect`, which means the caller is responsible for calling this type's `apply_effect` // at least once before returning to safe code. - ::get_components(target, func); + unsafe { ::get_components(target, func) }; // Forget the pointer so that the value is available in `apply_effect`. mem::forget(ptr); } @@ -372,7 +372,7 @@ impl DynamicBundle for SpawnOneRelated { // called exactly once for each component being fetched with the correct `StorageType` // - `Effect: !NoBundleEffect`, which means the caller is responsible for calling this type's `apply_effect` // at least once before returning to safe code. - ::get_components(target, func); + unsafe { ::get_components(target, func) }; // Forget the pointer so that the value is available in `apply_effect`. mem::forget(ptr); } diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index bab0aad03c443..8ea20fc5c21fd 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -54,7 +54,8 @@ impl BlobArray { capacity, } } else { - let mut arr = Self::with_capacity(item_layout, drop_fn, 0); + // SAFETY: Upheld by caller + let mut arr = unsafe { Self::with_capacity(item_layout, drop_fn, 0) }; // SAFETY: `capacity` > 0 unsafe { arr.alloc(NonZeroUsize::new_unchecked(capacity)) } arr diff --git a/crates/bevy_ecs/src/storage/mod.rs b/crates/bevy_ecs/src/storage/mod.rs index 0b163b1b4feca..0584ae15c6738 100644 --- a/crates/bevy_ecs/src/storage/mod.rs +++ b/crates/bevy_ecs/src/storage/mod.rs @@ -102,7 +102,8 @@ impl VecExtensions for Vec { // SAFETY: We replace self[index] with the last element. The caller must ensure that // both the last element and `index` must be valid and cannot point to the same place. unsafe { core::ptr::copy_nonoverlapping(base_ptr.add(len - 1), base_ptr.add(index), 1) }; - self.set_len(len - 1); + // SAFETY: Upheld by caller + unsafe { self.set_len(len - 1) }; value } } diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 239a7bfeb7244..903147449c849 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -336,7 +336,8 @@ impl Column { /// - `T` must match the type of data that's stored in this [`Column`] /// - `len` must match the actual length of this column (number of elements stored) pub unsafe fn get_data_slice(&self, len: usize) -> &[UnsafeCell] { - self.data.get_sub_slice(len) + // SAFETY: Upheld by caller + unsafe { self.data.get_sub_slice(len) } } /// Get a slice to the added [`ticks`](Tick) in this [`Column`]. @@ -344,7 +345,8 @@ impl Column { /// # Safety /// - `len` must match the actual length of this column (number of elements stored) pub unsafe fn get_added_ticks_slice(&self, len: usize) -> &[UnsafeCell] { - self.added_ticks.as_slice(len) + // SAFETY: Upheld by caller + unsafe { self.added_ticks.as_slice(len) } } /// Get a slice to the changed [`ticks`](Tick) in this [`Column`]. @@ -352,7 +354,8 @@ impl Column { /// # Safety /// - `len` must match the actual length of this column (number of elements stored) pub unsafe fn get_changed_ticks_slice(&self, len: usize) -> &[UnsafeCell] { - self.changed_ticks.as_slice(len) + // SAFETY: Upheld by caller + unsafe { self.changed_ticks.as_slice(len) } } /// Get a slice to the calling locations that last changed each value in this [`Column`] @@ -402,7 +405,8 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_added_tick_unchecked(&self, row: TableRow) -> &UnsafeCell { - self.added_ticks.get_unchecked(row.index()) + // SAFETY: Upheld by caller + unsafe { self.added_ticks.get_unchecked(row.index()) } } /// Fetches the "changed" change detection tick for the value at `row` @@ -412,7 +416,8 @@ impl Column { /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_changed_tick_unchecked(&self, row: TableRow) -> &UnsafeCell { - self.changed_ticks.get_unchecked(row.index()) + // SAFETY: Upheld by caller + unsafe { self.changed_ticks.get_unchecked(row.index()) } } /// Fetches the change detection ticks for the value at `row`. diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index b05711569ea0b..e51c49d82e771 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -403,11 +403,14 @@ where input: SystemIn<'_, Self>, world: UnsafeWorldCell, ) -> Result { - let value = self.a.run_unsafe(input, world)?; - // `Self::validate_param_unsafe` already validated the first system, - // but we still need to validate the second system once the first one runs. - self.b.validate_param_unsafe(world)?; - self.b.run_unsafe(value, world) + // SAFETY: Upheld by caller + unsafe { + let value = self.a.run_unsafe(input, world)?; + // `Self::validate_param_unsafe` already validated the first system, + // but we still need to validate the second system once the first one runs. + self.b.validate_param_unsafe(world)?; + self.b.run_unsafe(value, world) + } } #[cfg(feature = "hotpatching")] diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 8422ceacaa643..6df15e84c7492 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -182,11 +182,14 @@ const _: () = { system_meta: &bevy_ecs::system::SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - <__StructFieldsAlias as bevy_ecs::system::SystemParam>::validate_param( - &mut state.state, - system_meta, - world, - ) + // SAFETY: Upheld by caller + unsafe { + <__StructFieldsAlias as bevy_ecs::system::SystemParam>::validate_param( + &mut state.state, + system_meta, + world, + ) + } } #[inline] @@ -196,12 +199,15 @@ const _: () = { world: UnsafeWorldCell<'w>, change_tick: bevy_ecs::change_detection::Tick, ) -> Self::Item<'w, 's> { - let params = <__StructFieldsAlias as bevy_ecs::system::SystemParam>::get_param( - &mut state.state, - system_meta, - world, - change_tick, - ); + // SAFETY: Upheld by caller + let params = unsafe { + <__StructFieldsAlias as bevy_ecs::system::SystemParam>::get_param( + &mut state.state, + system_meta, + world, + change_tick, + ) + }; Commands { queue: InternalQueue::CommandQueue(params.0), allocator: params.1, diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 2481bb5f25f4f..8116f6c06b400 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -66,7 +66,8 @@ where _input: SystemIn<'_, Self>, world: UnsafeWorldCell, ) -> Result { - self.system.run_unsafe(&mut self.value, world) + // SAFETY: Upheld by caller + unsafe { self.system.run_unsafe(&mut self.value, world) } } #[cfg(feature = "hotpatching")] @@ -87,7 +88,8 @@ where &mut self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - self.system.validate_param_unsafe(world) + // SAFETY: Upheld by caller + unsafe { self.system.validate_param_unsafe(world) } } fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { @@ -163,7 +165,8 @@ where .value .as_mut() .expect("System input value was not found. Did you forget to initialize the system before running it?"); - self.system.run_unsafe(value, world) + // SAFETY: Upheld by caller + unsafe { self.system.run_unsafe(value, world) } } #[cfg(feature = "hotpatching")] @@ -184,7 +187,8 @@ where &mut self, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - self.system.validate_param_unsafe(world) + // SAFETY: Upheld by caller + unsafe { self.system.validate_param_unsafe(world) } } fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index de15c850ccdb9..16af1523376e4 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -703,7 +703,10 @@ macro_rules! impl_param_set { system_meta: &SystemMeta, world: UnsafeWorldCell<'w>, ) -> Result<(), SystemParamValidationError> { - <($($param,)*) as SystemParam>::validate_param(state, system_meta, world) + // SAFETY: Upheld by caller + unsafe { + <($($param,)*) as SystemParam>::validate_param(state, system_meta, world) + } } #[inline] @@ -968,7 +971,8 @@ unsafe impl<'w> SystemParam for DeferredWorld<'w> { world: UnsafeWorldCell<'world>, _change_tick: Tick, ) -> Self::Item<'world, 'state> { - world.into_deferred() + // SAFETY: Upheld by caller + unsafe { world.into_deferred() } } } @@ -1807,9 +1811,12 @@ unsafe impl SystemParam for Option { world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - T::validate_param(state, system_meta, world) - .ok() - .map(|()| T::get_param(state, system_meta, world, change_tick)) + // SAFETY: Upheld by caller + unsafe { + T::validate_param(state, system_meta, world) + .ok() + .map(|()| T::get_param(state, system_meta, world, change_tick)) + } } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -1850,8 +1857,11 @@ unsafe impl SystemParam for Result, change_tick: Tick, ) -> Self::Item<'world, 'state> { - T::validate_param(state, system_meta, world) - .map(|()| T::get_param(state, system_meta, world, change_tick)) + // SAFETY: Upheld by caller + unsafe { + T::validate_param(state, system_meta, world) + .map(|()| T::get_param(state, system_meta, world, change_tick)) + } } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -1944,7 +1954,8 @@ unsafe impl SystemParam for If { system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - T::validate_param(state, system_meta, world).map_err(|mut e| { + // SAFETY: Upheld by caller + unsafe { T::validate_param(state, system_meta, world) }.map_err(|mut e| { e.skipped = true; e }) @@ -1957,7 +1968,8 @@ unsafe impl SystemParam for If { world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - If(T::get_param(state, system_meta, world, change_tick)) + // SAFETY: Upheld by caller. + If(unsafe { T::get_param(state, system_meta, world, change_tick) }) } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2001,7 +2013,8 @@ unsafe impl SystemParam for Vec { world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { for state in state { - T::validate_param(state, system_meta, world)?; + // SAFETY: Upheld by caller + unsafe { T::validate_param(state, system_meta, world)? }; } Ok(()) } @@ -2183,9 +2196,17 @@ macro_rules! impl_system_param_tuple { world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { let ($($param,)*) = state; - $( - $param::validate_param($param, system_meta, world)?; - )* + + #[allow( + unused_unsafe, + reason = "Zero-length tuples won't have any params to validate." + )] + // SAFETY: Upheld by caller + unsafe { + $( + $param::validate_param($param, system_meta, world)?; + )* + } Ok(()) } @@ -2197,11 +2218,19 @@ macro_rules! impl_system_param_tuple { change_tick: Tick, ) -> Self::Item<'w, 's> { let ($($param,)*) = state; + #[allow( - clippy::unused_unit, - reason = "Zero-length tuples won't have any params to get." + unused_unsafe, + reason = "Zero-length tuples won't have any params to validate." )] - ($($param::get_param($param, system_meta, world, change_tick),)*) + // SAFETY: Upheld by caller + unsafe { + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples won't have any params to get." + )] + ($($param::get_param($param, system_meta, world, change_tick),)*) + } } } }; @@ -2354,7 +2383,8 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - P::validate_param(state, system_meta, world) + // SAFETY: Upheld by caller + unsafe { P::validate_param(state, system_meta, world) } } #[inline] @@ -2639,7 +2669,8 @@ impl DynParamState for ParamState { system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - T::validate_param(&mut self.0, system_meta, world) + // SAFETY: Upheld by caller + unsafe { T::validate_param(&mut self.0, system_meta, world) } } } @@ -2670,7 +2701,8 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - state.0.validate_param(system_meta, world) + // SAFETY: Upheld by caller. + unsafe { state.0.validate_param(system_meta, world) } } #[inline] diff --git a/crates/bevy_ecs/src/world/entity_access/component_fetch.rs b/crates/bevy_ecs/src/world/entity_access/component_fetch.rs index ee074bcf20f78..18dce1d725708 100644 --- a/crates/bevy_ecs/src/world/entity_access/component_fetch.rs +++ b/crates/bevy_ecs/src/world/entity_access/component_fetch.rs @@ -140,21 +140,24 @@ unsafe impl DynamicComponentFetch for [ComponentId; N] { self, cell: UnsafeEntityCell<'_>, ) -> Result, EntityComponentError> { - <&Self>::fetch_ref(&self, cell) + // SAFETY: Uphelp by caller. + unsafe { <&Self>::fetch_ref(&self, cell) } } unsafe fn fetch_mut( self, cell: UnsafeEntityCell<'_>, ) -> Result, EntityComponentError> { - <&Self>::fetch_mut(&self, cell) + // SAFETY: Uphelp by caller. + unsafe { <&Self>::fetch_mut(&self, cell) } } unsafe fn fetch_mut_assume_mutable( self, cell: UnsafeEntityCell<'_>, ) -> Result, EntityComponentError> { - <&Self>::fetch_mut_assume_mutable(&self, cell) + // SAFETY: Uphelp by caller. + unsafe { <&Self>::fetch_mut_assume_mutable(&self, cell) } } } diff --git a/crates/bevy_ecs/src/world/entity_access/filtered.rs b/crates/bevy_ecs/src/world/entity_access/filtered.rs index 4459fc262e517..b72540e452b8a 100644 --- a/crates/bevy_ecs/src/world/entity_access/filtered.rs +++ b/crates/bevy_ecs/src/world/entity_access/filtered.rs @@ -325,7 +325,8 @@ impl<'w, 's> UnsafeFilteredEntityMut<'w, 's> { /// - The user must ensure that no aliasing violations occur when using the returned `FilteredEntityMut`. #[inline] pub unsafe fn into_mut(self) -> FilteredEntityMut<'w, 's> { - FilteredEntityMut::new(self.entity, self.access) + // SAFETY: Upheld by caller. + unsafe { FilteredEntityMut::new(self.entity, self.access) } } } diff --git a/crates/bevy_ecs/src/world/entity_access/world_mut.rs b/crates/bevy_ecs/src/world/entity_access/world_mut.rs index 97cb0e4007584..b8a816d84835e 100644 --- a/crates/bevy_ecs/src/world/entity_access/world_mut.rs +++ b/crates/bevy_ecs/src/world/entity_access/world_mut.rs @@ -859,8 +859,11 @@ impl<'w> EntityWorldMut<'w> { &mut self, component_ids: F, ) -> Result, EntityComponentError> { - self.as_mutable() - .into_mut_assume_mutable_by_id(component_ids) + // SAFETY: Upheld by caller + unsafe { + self.as_mutable() + .into_mut_assume_mutable_by_id(component_ids) + } } /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) @@ -929,8 +932,11 @@ impl<'w> EntityWorldMut<'w> { self, component_ids: F, ) -> Result, EntityComponentError> { - self.into_mutable() - .into_mut_assume_mutable_by_id(component_ids) + // SAFETY: Upheld by caller + unsafe { + self.into_mutable() + .into_mut_assume_mutable_by_id(component_ids) + } } /// Adds a [`Bundle`] of components to the entity. @@ -1060,13 +1066,16 @@ impl<'w> EntityWorldMut<'w> { component_id: ComponentId, component: OwningPtr<'_>, ) -> &mut Self { - self.insert_by_id_with_caller( - component_id, - component, - InsertMode::Replace, - MaybeLocation::caller(), - RelationshipHookMode::Run, - ) + // SAFETY: Upheld by caller + unsafe { + self.insert_by_id_with_caller( + component_id, + component, + InsertMode::Replace, + MaybeLocation::caller(), + RelationshipHookMode::Run, + ) + } } /// # Safety diff --git a/crates/bevy_ecs/src/world/entity_fetch.rs b/crates/bevy_ecs/src/world/entity_fetch.rs index 1c2e2d2fcf705..457f176d0457d 100644 --- a/crates/bevy_ecs/src/world/entity_fetch.rs +++ b/crates/bevy_ecs/src/world/entity_fetch.rs @@ -247,7 +247,8 @@ unsafe impl WorldEntityFetch for [Entity; N] { self, cell: UnsafeWorldCell<'_>, ) -> Result, EntityNotSpawnedError> { - <&Self>::fetch_ref(&self, cell) + // SAFETY: Upheld by caller + unsafe { <&Self>::fetch_ref(&self, cell) } } #[inline] @@ -255,7 +256,8 @@ unsafe impl WorldEntityFetch for [Entity; N] { self, cell: UnsafeWorldCell<'_>, ) -> Result, EntityMutableFetchError> { - <&Self>::fetch_mut(&self, cell) + // SAFETY: Upheld by caller + unsafe { <&Self>::fetch_mut(&self, cell) } } #[inline] @@ -263,7 +265,8 @@ unsafe impl WorldEntityFetch for [Entity; N] { self, cell: UnsafeWorldCell<'_>, ) -> Result, EntityMutableFetchError> { - <&Self>::fetch_deferred_mut(&self, cell) + // SAFETY: Upheld by caller + unsafe { <&Self>::fetch_deferred_mut(&self, cell) } } } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 54a835e2863ab..cae3f5d333db7 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -697,7 +697,8 @@ impl<'w> UnsafeWorldCell<'w> { /// Must have read access to [`DefaultErrorHandler`]. #[inline] pub unsafe fn default_error_handler(&self) -> ErrorHandler { - self.get_resource::() + // SAFETY: Upheld by caller + unsafe { self.get_resource::() } .copied() .unwrap_or_default() .0 From 86224b5cb2e08ca4e0cc51d32a7bc74939baa3b6 Mon Sep 17 00:00:00 2001 From: Chris Biscardi Date: Sun, 14 Dec 2025 18:18:20 -0800 Subject: [PATCH 120/133] Refactor GltfExtensionHandler Hooks (#22114) As it turns out, many glTF extensions require accessing the data from other extensions. Trying to isolate and scope access is much less helpful than exposing the glTF objects for consumers to take what they want. This includes - extension data in the "others" category (anything the gltf crate doesn't support explicitly) - the functions that return the extension data the gltf crate *does* have hardcoded support for - names and any other available data --- The diff for users is that they - no longer have to worry about defining extension ids to process - no longer have to worry about multiple calls due to those ids - now have to use `.extension_value()`, `.name()`, or similar to get relevant data - Can now access `.light()` or any other data built-in to the gltf crate, such as [`.variants`](https://docs.rs/gltf/1.4.1/gltf/struct.Document.html#method.variants) for `KHR_materials_variants`. An example diff on the user side can be viewed in the update commit for the Skein PR: https://github.com/rust-adventure/skein/pull/89/changes/ac1e510c9d136d60c23decd68b10f35f6e24f76b --- crates/bevy_gltf/src/loader/extensions/mod.rs | 74 +++--------- crates/bevy_gltf/src/loader/mod.rs | 110 ++++-------------- .../gltf/gltf_extension_animation_graph.rs | 18 +-- 3 files changed, 40 insertions(+), 162 deletions(-) diff --git a/crates/bevy_gltf/src/loader/extensions/mod.rs b/crates/bevy_gltf/src/loader/extensions/mod.rs index 32ec5ce6a7fb5..133420881a9cf 100644 --- a/crates/bevy_gltf/src/loader/extensions/mod.rs +++ b/crates/bevy_gltf/src/loader/extensions/mod.rs @@ -47,46 +47,26 @@ pub struct GltfExtensionHandlers(pub Arc Box; - /// When loading a glTF file, a glTF object that could contain extension - /// data will cause the relevant hook to execute once for each id in this list. - /// Each invocation will receive the extension data for one of the extension ids, - /// along with the `extension_id` itself so implementors can differentiate - /// between different calls and parse data correctly. - /// - /// The hooks are always called, even if there is no extension data - /// for a specified id. This is useful for scenarios where additional - /// extension data isn't required, but processing should still happen. - /// - /// Most implementors will pick one extension for this list, causing the - /// relevant hooks to fire once per object. An implementor that does not - /// wish to receive any data but still wants hooks to be called can use - /// an empty string `""` as the extension id, which is also the default - /// value if the function is not implemented by an implementor. If the - /// empty string is used, all extension data in hooks will be `None`. - /// - /// Some implementors will choose to list multiple extensions here. - /// This is an advanced use case and the alternative of having multiple - /// independent handlers should be considered as an option first. - /// If multiple extension ids are listed here, the hooks will fire once - /// for each extension id, and each successive call will receive the data for - /// a separate extension. The extension id is also included in hook arguments - /// for this reason, so multiple extension id implementors can differentiate - /// between the data received. - fn extension_ids(&self) -> &'static [&'static str] { - &[""] - } - /// Called when the "global" data for an extension /// at the root of a glTF file is encountered. #[expect( unused, reason = "default trait implementations do not use the arguments because they are no-ops" )] - fn on_root_data(&mut self, extension_id: &str, value: Option<&serde_json::Value>) {} + fn on_root(&mut self, gltf: &gltf::Gltf) {} #[cfg(feature = "bevy_animation")] #[expect( @@ -94,15 +74,7 @@ pub trait GltfExtensionHandler: Send + Sync { reason = "default trait implementations do not use the arguments because they are no-ops" )] /// Called when an individual animation is processed - fn on_animation( - &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, - gltf_animation: &gltf::Animation, - name: Option<&str>, - handle: Handle, - ) { - } + fn on_animation(&mut self, gltf_animation: &gltf::Animation, handle: Handle) {} #[cfg(feature = "bevy_animation")] #[expect( @@ -123,14 +95,15 @@ pub trait GltfExtensionHandler: Send + Sync { } /// Called when an individual texture is processed + /// Unlike other hooks, this hook does not receive its glTF + /// object due to internal constraints. #[expect( unused, reason = "default trait implementations do not use the arguments because they are no-ops" )] fn on_texture( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, + extension_data: Option<&serde_json::Map>, texture: Handle, ) { } @@ -142,11 +115,8 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_material( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, load_context: &mut LoadContext<'_>, gltf_material: &gltf::Material, - name: Option<&str>, material: Handle, ) { } @@ -158,11 +128,8 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_gltf_mesh( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, load_context: &mut LoadContext<'_>, gltf_mesh: &gltf::Mesh, - name: Option<&str>, mesh: Handle, ) { } @@ -191,13 +158,10 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_scene_completed( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, + load_context: &mut LoadContext<'_>, scene: &gltf::Scene, - name: Option<&str>, world_root_id: Entity, scene_world: &mut World, - load_context: &mut LoadContext<'_>, ) { } @@ -208,8 +172,6 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_gltf_node( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, load_context: &mut LoadContext<'_>, gltf_node: &Node, entity: &mut EntityWorldMut, @@ -225,8 +187,6 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_spawn_light_directional( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, load_context: &mut LoadContext<'_>, gltf_node: &Node, entity: &mut EntityWorldMut, @@ -241,8 +201,6 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_spawn_light_point( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, load_context: &mut LoadContext<'_>, gltf_node: &Node, entity: &mut EntityWorldMut, @@ -257,8 +215,6 @@ pub trait GltfExtensionHandler: Send + Sync { )] fn on_spawn_light_spot( &mut self, - extension_id: &str, - extension_data: Option<&serde_json::Value>, load_context: &mut LoadContext<'_>, gltf_node: &Node, entity: &mut EntityWorldMut, diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 1c5bbd94e74c1..e4ecf35f814d4 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -258,9 +258,7 @@ impl GltfLoader { // Let extensions process the root data for the extension ids // they've subscribed to. for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_root_data(id, gltf.extension_value(id)); - } + extension.on_root(&gltf); } let file_name = load_context @@ -597,15 +595,7 @@ impl GltfLoader { // let extensions handle extension data placed on animations for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_animation( - id, - animation.extension_value(id), - &animation, - animation.name(), - handle.clone(), - ); - } + extension.on_animation(&animation, handle.clone()); } animations.push(handle); @@ -654,13 +644,10 @@ impl GltfLoader { image.process_loaded_texture(load_context, &mut texture_handles); // let extensions handle texture data for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_texture( - id, - texture.extension_value(id), - texture_handles.iter().last().unwrap().clone(), - ); - } + extension.on_texture( + texture.extensions(), + texture_handles.iter().last().unwrap().clone(), + ); } } } else { @@ -695,13 +682,10 @@ impl GltfLoader { // We do this differently here because of the IoTaskPool vs // gltf::Texture lifetimes for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_texture( - id, - extension_data.as_ref().and_then(|map| map.get(*id)), - texture_handles.iter().last().unwrap().clone(), - ); - } + extension.on_texture( + extension_data.as_ref(), + texture_handles.iter().last().unwrap().clone(), + ); } } Err(err) => { @@ -723,16 +707,7 @@ impl GltfLoader { // let extensions handle material data for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_material( - id, - material.extension_value(id), - load_context, - &material, - material.name(), - handle.clone(), - ); - } + extension.on_material(load_context, &material, handle.clone()); } materials.push(handle); @@ -899,16 +874,7 @@ impl GltfLoader { named_meshes.insert(name.into(), handle.clone()); } for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_gltf_mesh( - id, - gltf_mesh.extension_value(id), - load_context, - &gltf_mesh, - gltf_mesh.name(), - handle.clone(), - ); - } + extension.on_gltf_mesh(load_context, &gltf_mesh, handle.clone()); } meshes.push(handle); @@ -1114,17 +1080,12 @@ impl GltfLoader { // let extensions handle scene extension data for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_scene_completed( - id, - scene.extension_value(id), - &scene, - scene.name(), - world_root_id, - &mut world, - &mut scene_load_context, - ); - } + extension.on_scene_completed( + &mut scene_load_context, + &scene, + world_root_id, + &mut world, + ); } let loaded_scene = scene_load_context.finish(Scene::new(world)); @@ -1755,15 +1716,7 @@ fn load_node( }); } for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_spawn_light_directional( - id, - gltf_node.extension_value(id), - load_context, - gltf_node, - &mut entity, - ); - } + extension.on_spawn_light_directional(load_context, gltf_node, &mut entity); } } gltf::khr_lights_punctual::Kind::Point => { @@ -1786,15 +1739,7 @@ fn load_node( }); } for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_spawn_light_point( - id, - gltf_node.extension_value(id), - load_context, - gltf_node, - &mut entity, - ); - } + extension.on_spawn_light_point(load_context, gltf_node, &mut entity); } } gltf::khr_lights_punctual::Kind::Spot { @@ -1822,15 +1767,7 @@ fn load_node( }); } for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - extension.on_spawn_light_spot( - id, - gltf_node.extension_value(id), - load_context, - gltf_node, - &mut entity, - ); - } + extension.on_spawn_light_spot(load_context, gltf_node, &mut entity); } } } @@ -1881,10 +1818,7 @@ fn load_node( // accessing Mesh and Material extension data, which // are merged onto the same entity in Bevy for extension in extensions.iter_mut() { - for id in extension.extension_ids() { - let data = gltf_node.extension_value(id); - extension.on_gltf_node(id, data, load_context, gltf_node, &mut node); - } + extension.on_gltf_node(load_context, gltf_node, &mut node); } if let Some(err) = gltf_error { diff --git a/examples/gltf/gltf_extension_animation_graph.rs b/examples/gltf/gltf_extension_animation_graph.rs index dea4112a980b9..7c0d8d0748426 100644 --- a/examples/gltf/gltf_extension_animation_graph.rs +++ b/examples/gltf/gltf_extension_animation_graph.rs @@ -135,15 +135,8 @@ impl GltfExtensionHandler for GltfExtensionHandlerAnimation { } #[cfg(feature = "bevy_animation")] - fn on_animation( - &mut self, - _extension_id: &str, - _value: Option<&serde_json::Value>, - _gltf_animation: &gltf::Animation, - name: Option<&str>, - handle: Handle, - ) { - if name.is_some_and(|v| v == "Walk") { + fn on_animation(&mut self, gltf_animation: &gltf::Animation, handle: Handle) { + if gltf_animation.name().is_some_and(|v| v == "Walk") { self.clip = Some(handle.clone()); } } @@ -160,8 +153,6 @@ impl GltfExtensionHandler for GltfExtensionHandlerAnimation { fn on_gltf_node( &mut self, - _extension_id: &str, - _value: Option<&serde_json::Value>, _load_context: &mut LoadContext<'_>, gltf_node: &gltf::Node, entity: &mut EntityWorldMut, @@ -174,13 +165,10 @@ impl GltfExtensionHandler for GltfExtensionHandlerAnimation { /// Called when an individual Scene is done processing fn on_scene_completed( &mut self, - _extension_id: &str, - _value: Option<&serde_json::Value>, + load_context: &mut LoadContext<'_>, _scene: &gltf::Scene, - _name: Option<&str>, _world_root_id: Entity, world: &mut World, - load_context: &mut LoadContext<'_>, ) { // Create an AnimationGraph from the desired clip let (graph, index) = AnimationGraph::from_clip(self.clip.clone().unwrap()); From 82acdfdacae2d14b9079ee1b02484293dcea77a1 Mon Sep 17 00:00:00 2001 From: databasedav <31483365+databasedav@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:37:43 -0800 Subject: [PATCH 121/133] derive `Clone` and `Copy` for newer `interaction_states` unit structs (#22115) # Objective - derive `Clone` and `Copy` for the newer unit structs in `interaction_states`, this is useful for frameworks that are generic over `Component + Clone/Copy` and is also the status quo for the existing `InteractionDisabled` component defined a few lines above ## Solution - derive `Clone` and `Copy` for `Pressed`, `Checkable`, and `Checked` --- crates/bevy_ui/src/interaction_states.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index a5793ca778a57..41e80114062a9 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -37,15 +37,15 @@ pub(crate) fn on_remove_disabled( /// Component that indicates whether a button or widget is currently in a pressed or "held down" /// state. -#[derive(Component, Default, Debug)] +#[derive(Component, Debug, Clone, Copy, Default)] pub struct Pressed; /// Component that indicates that a widget can be checked. -#[derive(Component, Default, Debug)] +#[derive(Component, Debug, Clone, Copy, Default)] pub struct Checkable; /// Component that indicates whether a checkbox or radio button is in a checked state. -#[derive(Component, Default, Debug)] +#[derive(Component, Debug, Clone, Copy, Default)] pub struct Checked; pub(crate) fn on_add_checkable(add: On, mut world: DeferredWorld) { From 45955c72b9e4659613ed1a4726c182ab0eb181b8 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Mon, 15 Dec 2025 00:20:21 -0800 Subject: [PATCH 122/133] Use slice `last` instead of iter `last. (#22116) # Objective - Followup to #22106. ## Solution - Use slice `last` instead of iter `last` for better performance - we don't need to iterate just to find the last element. --- crates/bevy_gltf/src/loader/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index e4ecf35f814d4..68e5eb41dc69d 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -646,7 +646,7 @@ impl GltfLoader { for extension in extensions.iter_mut() { extension.on_texture( texture.extensions(), - texture_handles.iter().last().unwrap().clone(), + texture_handles.last().unwrap().clone(), ); } } @@ -684,7 +684,7 @@ impl GltfLoader { for extension in extensions.iter_mut() { extension.on_texture( extension_data.as_ref(), - texture_handles.iter().last().unwrap().clone(), + texture_handles.last().unwrap().clone(), ); } } From 2765ba8803d6a040526edfa6af5cdad25c0f6c6f Mon Sep 17 00:00:00 2001 From: andriyDev Date: Mon, 15 Dec 2025 00:30:43 -0800 Subject: [PATCH 123/133] Fix compilation errors and warnings when running bevy_gltf with no features. (#22125) # Objective - #22106 accidentally added imports for AnimationClip, HashMap, and HashSet - but these are only available/used if the bevy_animation feature is enabled. So running `cargo t -p bevy_gltf` fails to compile and gives warnings! ## Solution - Guard these `use` statements on the bevy_animation feature. ## Testing - Ran `cargo t -p bevy_gltf`. - Ran `cargo t -p bevy_gltf --all-features`. --- crates/bevy_gltf/src/loader/extensions/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/bevy_gltf/src/loader/extensions/mod.rs b/crates/bevy_gltf/src/loader/extensions/mod.rs index 133420881a9cf..d024f39175c94 100644 --- a/crates/bevy_gltf/src/loader/extensions/mod.rs +++ b/crates/bevy_gltf/src/loader/extensions/mod.rs @@ -7,7 +7,6 @@ mod khr_materials_specular; use alloc::sync::Arc; use async_lock::RwLock; -use bevy_animation::AnimationClip; use bevy_asset::{Handle, LoadContext}; use bevy_ecs::{ entity::Entity, @@ -15,9 +14,14 @@ use bevy_ecs::{ world::{EntityWorldMut, World}, }; use bevy_pbr::StandardMaterial; -use bevy_platform::collections::{HashMap, HashSet}; use gltf::Node; +#[cfg(feature = "bevy_animation")] +use { + bevy_animation::AnimationClip, + bevy_platform::collections::{HashMap, HashSet}, +}; + use crate::GltfMesh; pub(crate) use self::{ From 545a97d67cdb87237e0bc5f02ecd17ec7e4b5c24 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Mon, 15 Dec 2025 15:24:36 -0500 Subject: [PATCH 124/133] Fix: Clears directional navigation map between rebuilds (#22124) # Objective - Closes #21949 ## Solution - Implements the solution as suggested by 21949. The Directional Navigation Map does not prune any old edges that may be connected to removed Nodes, so we clear the map between rebuilds of the map. (If the more preferable solution figures out what nodes were removed and then calls `map.remove_multiple` instead, you can feel free to reject this) ## Testing - Did you test these changes? If so, how? I did not test this change, but the reporter of the issue did (via backport to an older version of Bevy). However, the fix is straightforward and explains itself. If you would like an automated test somehow, some pointers to tests that do similar would be welcome. --- crates/bevy_input_focus/src/directional_navigation.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index ed34e84bcc067..b4ce6ff319e5b 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -773,6 +773,8 @@ fn auto_rebuild_ui_navigation_graph( }) .collect(); + // clear the old nav map between rebuilds to ensure any removed entities' edges are pruned + directional_nav_map.clear(); auto_generate_navigation_edges(&mut directional_nav_map, &nodes, &config); } From 59717100d99b7cb0370770be63e1adc5db0d7a2c Mon Sep 17 00:00:00 2001 From: atlv Date: Mon, 15 Dec 2025 15:28:45 -0500 Subject: [PATCH 125/133] Global Gizmos (#22107) # Objective - use gizmos in non-system contexts, such as deep in a math library or callback without ecs access. - kind of upstream https://github.com/atlv24/glizmo - builds off of https://github.com/bevyengine/bevy/pull/22105 ## Solution - mutex + deref ## Testing - --- crates/bevy_gizmos/src/global.rs | 48 ++++++++++++++++++++++++++++++++ crates/bevy_gizmos/src/lib.rs | 4 ++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 crates/bevy_gizmos/src/global.rs diff --git a/crates/bevy_gizmos/src/global.rs b/crates/bevy_gizmos/src/global.rs new file mode 100644 index 0000000000000..14d180b066ac2 --- /dev/null +++ b/crates/bevy_gizmos/src/global.rs @@ -0,0 +1,48 @@ +use std::sync::Mutex; + +use bevy_app::{App, Last, Plugin}; +use bevy_ecs::schedule::IntoScheduleConfigs; + +use crate::{ + config::DefaultGizmoConfigGroup, + gizmos::{GizmoBuffer, Gizmos}, + GizmoMeshSystems, +}; + +static GLOBAL_GIZMO: Mutex> = + Mutex::new(GizmoBuffer::new()); + +/// Lets you use bevy gizmos outside of systems. +pub struct GlobalGizmosPlugin; + +impl Plugin for GlobalGizmosPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Last, flush_global_gizmos.before(GizmoMeshSystems)); + } +} + +fn flush_global_gizmos(mut gizmos: Gizmos) { + let mut buffer = GizmoBuffer::new(); + { + core::mem::swap(&mut buffer, &mut GLOBAL_GIZMO.lock().unwrap()); + } + gizmos.strip_positions.extend(buffer.strip_positions); + gizmos.strip_colors.extend(buffer.strip_colors); + gizmos.list_positions.extend(buffer.list_positions); + gizmos.list_colors.extend(buffer.list_colors); +} + +/// A global gizmo context for use outside of bevy systems. +/// +/// # Example +/// ``` +/// # use bevy_gizmos::prelude::*; +/// # use bevy_math::prelude::*; +/// # use bevy_color::palettes::basic::WHITE; +/// fn draw() { +/// gizmo().sphere(Isometry3d::IDENTITY, 0.5, WHITE); +/// } +/// ``` +pub fn gizmo() -> impl core::ops::DerefMut> { + GLOBAL_GIZMO.lock().unwrap() +} diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 9d6f6500f1476..a693c43d26e80 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -30,6 +30,7 @@ pub mod config; pub mod cross; pub mod curves; pub mod gizmos; +mod global; pub mod grid; pub mod primitives; pub mod retained; @@ -52,6 +53,7 @@ pub mod prelude { GizmoLineConfig, GizmoLineJoint, GizmoLineStyle, }, gizmos::Gizmos, + global::gizmo, primitives::{dim2::GizmoPrimitive2d, dim3::GizmoPrimitive3d}, retained::Gizmo, AppGizmoBuilder, GizmoAsset, @@ -92,7 +94,7 @@ impl Plugin for GizmoPlugin { // We insert the Resource GizmoConfigStore into the world implicitly here if it does not exist. .init_gizmo_group::(); - app.add_plugins(aabb::AabbGizmoPlugin); + app.add_plugins((aabb::AabbGizmoPlugin, global::GlobalGizmosPlugin)); #[cfg(feature = "bevy_light")] app.add_plugins(LightGizmoPlugin); From 0303bd664b76ea0ae5ed1473545467cf30cebcfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Mon, 15 Dec 2025 21:30:09 +0100 Subject: [PATCH 126/133] disable screenrecording on windows (#22130) # Objective - Screen Recording is not ready to be exposed, and very hard to build on windows ## Solution - Disable it for now on windows --------- Co-authored-by: Alice Cecile Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> --- crates/bevy_dev_tools/Cargo.toml | 4 +- crates/bevy_dev_tools/src/easy_screenshot.rs | 246 +++++++++++------- .../release-notes/easy_marketing_material.md | 2 + 3 files changed, 155 insertions(+), 97 deletions(-) diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 36854b3f5088e..9d1506356e215 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["bevy"] [features] bevy_ci_testing = ["serde", "ron"] -screenrecording = ["x264"] +screenrecording = ["dep:x264"] webgl = ["bevy_render/webgl"] webgpu = ["bevy_render/webgpu"] @@ -40,6 +40,8 @@ bevy_state = { path = "../bevy_state", version = "0.18.0-dev" } serde = { version = "1.0", features = ["derive"], optional = true } ron = { version = "0.12", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } + +[target.'cfg(not(target_os = "windows"))'.dependencies] x264 = { version = "0.5.0", optional = true } [lints] diff --git a/crates/bevy_dev_tools/src/easy_screenshot.rs b/crates/bevy_dev_tools/src/easy_screenshot.rs index 4b5933038313e..84f93e9879820 100644 --- a/crates/bevy_dev_tools/src/easy_screenshot.rs +++ b/crates/bevy_dev_tools/src/easy_screenshot.rs @@ -1,25 +1,13 @@ #[cfg(feature = "screenrecording")] use core::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(feature = "screenrecording")] -use std::{fs::File, io::Write, sync::mpsc::channel}; use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; -#[cfg(feature = "screenrecording")] -use bevy_image::Image; use bevy_input::{common_conditions::input_just_pressed, keyboard::KeyCode}; -#[cfg(feature = "screenrecording")] -use bevy_render::view::screenshot::ScreenshotCaptured; use bevy_render::view::screenshot::{save_to_disk, Screenshot}; -#[cfg(feature = "screenrecording")] -use bevy_time::Time; use bevy_window::{PrimaryWindow, Window}; -#[cfg(feature = "screenrecording")] -use tracing::info; -#[cfg(feature = "screenrecording")] -use x264::{Colorspace, Encoder, Setup}; -#[cfg(feature = "screenrecording")] +#[cfg(all(not(target_os = "windows"), feature = "screenrecording"))] pub use x264::{Preset, Tune}; /// File format the screenshot will be saved in @@ -82,6 +70,50 @@ impl Plugin for EasyScreenshotPlugin { } } +/// Placeholder +#[cfg(all(target_os = "windows", feature = "screenrecording"))] +pub enum Preset { + /// Placeholder + Ultrafast, + /// Placeholder + Superfast, + /// Placeholder + Veryfast, + /// Placeholder + Faster, + /// Placeholder + Fast, + /// Placeholder + Medium, + /// Placeholder + Slow, + /// Placeholder + Slower, + /// Placeholder + Veryslow, + /// Placeholder + Placebo, +} + +/// Placeholder +#[cfg(all(target_os = "windows", feature = "screenrecording"))] +pub enum Tune { + /// Placeholder + None, + /// Placeholder + Film, + /// Placeholder + Animation, + /// Placeholder + Grain, + /// Placeholder + StillImage, + /// Placeholder + Psnr, + /// Placeholder + Ssim, +} + #[cfg(feature = "screenrecording")] /// Add this plugin to your app to enable easy screen recording. pub struct EasyScreenRecordPlugin { @@ -107,13 +139,6 @@ impl Default for EasyScreenRecordPlugin { } } -#[cfg(feature = "screenrecording")] -enum RecordCommand { - Start(String, Preset, Tune), - Stop, - Frame(Image), -} - #[cfg(feature = "screenrecording")] /// Controls screen recording #[derive(Message)] @@ -126,90 +151,118 @@ pub enum RecordScreen { #[cfg(feature = "screenrecording")] impl Plugin for EasyScreenRecordPlugin { + #[cfg_attr( + target_os = "windows", + expect(unused_variables, reason = "not working on windows") + )] fn build(&self, app: &mut App) { - let (tx, rx) = channel::(); + #[cfg(target_os = "windows")] + { + tracing::warn!("Screen recording is not currently supported on Windows: see https://github.com/bevyengine/bevy/issues/22132"); + } + #[cfg(not(target_os = "windows"))] + { + use bevy_image::Image; + use bevy_render::view::screenshot::ScreenshotCaptured; + use bevy_time::Time; + use std::{fs::File, io::Write, sync::mpsc::channel}; + use tracing::info; + use x264::{Colorspace, Encoder, Setup}; - let frame_time = self.frame_time; + enum RecordCommand { + Start(String, Preset, Tune), + Stop, + Frame(Image), + } - std::thread::spawn(move || { - let mut encoder: Option = None; - let mut setup = None; - let mut file: Option = None; - let mut frame = 0; - loop { - let Ok(next) = rx.recv() else { - break; - }; - match next { - RecordCommand::Start(name, preset, tune) => { - info!("starting recording at {}", name); - file = Some(File::create(name).unwrap()); - setup = Some(Setup::preset(preset, tune, false, true).high()); - } - RecordCommand::Stop => { - info!("stopping recording"); - if let Some(encoder) = encoder.take() { - let mut flush = encoder.flush(); - let mut file = file.take().unwrap(); - while let Some(result) = flush.next() { - let (data, _) = result.unwrap(); - file.write_all(data.entirety()).unwrap(); - } + let (tx, rx) = channel::(); + + let frame_time = self.frame_time; + + std::thread::spawn(move || { + let mut encoder: Option = None; + let mut setup = None; + let mut file: Option = None; + let mut frame = 0; + loop { + let Ok(next) = rx.recv() else { + break; + }; + match next { + RecordCommand::Start(name, preset, tune) => { + info!("starting recording at {}", name); + file = Some(File::create(name).unwrap()); + setup = Some(Setup::preset(preset, tune, false, true).high()); } - } - RecordCommand::Frame(image) => { - if let Some(setup) = setup.take() { - let mut new_encoder = setup - .fps((1000 / frame_time.as_millis()) as u32, 1) - .build(Colorspace::RGB, image.width() as i32, image.height() as i32) - .unwrap(); - let headers = new_encoder.headers().unwrap(); - file.as_mut() - .unwrap() - .write_all(headers.entirety()) - .unwrap(); - encoder = Some(new_encoder); + RecordCommand::Stop => { + info!("stopping recording"); + if let Some(encoder) = encoder.take() { + let mut flush = encoder.flush(); + let mut file = file.take().unwrap(); + while let Some(result) = flush.next() { + let (data, _) = result.unwrap(); + file.write_all(data.entirety()).unwrap(); + } + } } - if let Some(encoder) = encoder.as_mut() { - let pts = (frame_time.as_millis() * frame) as i64; - - frame += 1; - let (data, _) = encoder - .encode( - pts, - x264::Image::rgb( + RecordCommand::Frame(image) => { + if let Some(setup) = setup.take() { + let mut new_encoder = setup + .fps((1000 / frame_time.as_millis()) as u32, 1) + .build( + Colorspace::RGB, image.width() as i32, image.height() as i32, - &image.try_into_dynamic().unwrap().to_rgb8(), - ), - ) - .unwrap(); - file.as_mut().unwrap().write_all(data.entirety()).unwrap(); + ) + .unwrap(); + let headers = new_encoder.headers().unwrap(); + file.as_mut() + .unwrap() + .write_all(headers.entirety()) + .unwrap(); + encoder = Some(new_encoder); + } + if let Some(encoder) = encoder.as_mut() { + let pts = (frame_time.as_millis() * frame) as i64; + + frame += 1; + let (data, _) = encoder + .encode( + pts, + x264::Image::rgb( + image.width() as i32, + image.height() as i32, + &image.try_into_dynamic().unwrap().to_rgb8(), + ), + ) + .unwrap(); + file.as_mut().unwrap().write_all(data.entirety()).unwrap(); + } } } } - } - }); + }); - let frame_time = self.frame_time; + let frame_time = self.frame_time; - app.add_message::().add_systems( - Update, - ( - (move |mut messages: MessageWriter, mut recording: Local| { - *recording = !*recording; - if *recording { - messages.write(RecordScreen::Start); - } else { - messages.write(RecordScreen::Stop); - } - }) - .run_if(input_just_pressed(self.toggle)), - { - let tx = tx.clone(); - let preset = self.preset; - let tune = self.tune; - move |mut commands: Commands, + app.add_message::().add_systems( + Update, + ( + (move |mut messages: MessageWriter, + mut recording: Local| { + *recording = !*recording; + if *recording { + messages.write(RecordScreen::Start); + } else { + messages.write(RecordScreen::Stop); + } + }) + .run_if(input_just_pressed(self.toggle)), + { + let tx = tx.clone(); + let preset = self.preset; + let tune = self.tune; + move |mut commands: Commands, mut recording: Local, mut messages: MessageReader, window: Single<&Window, With>, @@ -251,9 +304,10 @@ impl Plugin for EasyScreenRecordPlugin { ); } } - }, - ) - .chain(), - ); + }, + ) + .chain(), + ); + } } } diff --git a/release-content/release-notes/easy_marketing_material.md b/release-content/release-notes/easy_marketing_material.md index 953db7e9dcaf2..943a9b26a61e7 100644 --- a/release-content/release-notes/easy_marketing_material.md +++ b/release-content/release-notes/easy_marketing_material.md @@ -7,3 +7,5 @@ pull_requests: [21235, 21237] Bevy can take a screenshot of what's rendered since 0.11. This is now easier to setup to help you create marketing material, so that you can take screenshot with consistent formatting with the new `EasyScreenshotPlugin`. With its default settings, once you add this plugin to your application, a PNG screenshot will be taken when you press the `PrintScreen` key. You can change the trigger key, or the screenshot format between PNG, JPEG or BMP. It is now possible to record a movie from Bevy, with the new `EasyScreenRecordPlugin`. This plugins add a toggle key, space bar by default, that will toggle screen recording. Recording can also be started and stopped programmatically with the `RecordScreen` messages. + +Screen recording is not working for now on Windows. From 5ac16eb00845ac97a9a625b5ac1554b0b92b6c58 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Mon, 15 Dec 2025 15:36:10 -0500 Subject: [PATCH 127/133] Fix: Ignores nodes that are hidden via their parents in directional nav (#22123) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Fixes #21950 ## Solution - Updated the query as suggested in the issue to also check for `InheritedVisibility`. Entities that have `Visibility::INHERITED` and `InheritedVisibility::HIDDEN` should also be ignored for directional navigation ## Testing - Did you test these changes? If so, how? I did not test this change at all! And I wasn’t sure how best to set up an automated test for this since there is not an existing one for that function, else I would have. However, the logic change is simple at least… So if a test is desired, just let me know and please provide me with a little direction :) --- .../src/directional_navigation.rs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs index b4ce6ff319e5b..98e6407a1a9c7 100644 --- a/crates/bevy_input_focus/src/directional_navigation.rs +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -60,7 +60,7 @@ use alloc::vec::Vec; use bevy_app::prelude::*; -use bevy_camera::visibility::Visibility; +use bevy_camera::visibility::{InheritedVisibility, Visibility}; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, prelude::*, @@ -749,6 +749,7 @@ fn auto_rebuild_ui_navigation_graph( &ComputedNode, &UiGlobalTransform, Option<&Visibility>, + Option<&InheritedVisibility>, ), With, >, @@ -759,18 +760,24 @@ fn auto_rebuild_ui_navigation_graph( let nodes: Vec = all_nodes .iter() - .filter_map(|(entity, computed, transform, visibility)| { - // Skip hidden or zero-size nodes - if computed.is_empty() || matches!(visibility, Some(Visibility::Hidden)) { - return None; - } - let (_scale, _rotation, translation) = transform.to_scale_angle_translation(); - Some(FocusableArea { - entity, - position: translation, - size: computed.size(), - }) - }) + .filter_map( + |(entity, computed, transform, visibility, inherited_visibility)| { + // Skip hidden or zero-size nodes + if computed.is_empty() + || matches!(visibility, Some(Visibility::Hidden)) + || (matches!(visibility, Some(Visibility::Inherited)) + && matches!(inherited_visibility, Some(&InheritedVisibility::HIDDEN))) + { + return None; + } + let (_scale, _rotation, translation) = transform.to_scale_angle_translation(); + Some(FocusableArea { + entity, + position: translation, + size: computed.size(), + }) + }, + ) .collect(); // clear the old nav map between rebuilds to ensure any removed entities' edges are pruned From 3ab5c2c746506ecee720c11648ed31b98c543088 Mon Sep 17 00:00:00 2001 From: Chris Biscardi Date: Mon, 15 Dec 2025 12:38:49 -0800 Subject: [PATCH 128/133] GltfExtensionHandler::on_texture: pass Texture references (#22131) # Objective extension hooks for `on_texture` currently pass the extension data and not the gltf object. ## Solution `IoTaskPool::scope` preserves ordering if the futures are only spawned from the root scope, so we can use that to zip with the texture references for hooks --- crates/bevy_gltf/src/loader/extensions/mod.rs | 9 +------- crates/bevy_gltf/src/loader/mod.rs | 22 ++++++------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/crates/bevy_gltf/src/loader/extensions/mod.rs b/crates/bevy_gltf/src/loader/extensions/mod.rs index d024f39175c94..370b767f723b1 100644 --- a/crates/bevy_gltf/src/loader/extensions/mod.rs +++ b/crates/bevy_gltf/src/loader/extensions/mod.rs @@ -99,18 +99,11 @@ pub trait GltfExtensionHandler: Send + Sync { } /// Called when an individual texture is processed - /// Unlike other hooks, this hook does not receive its glTF - /// object due to internal constraints. #[expect( unused, reason = "default trait implementations do not use the arguments because they are no-ops" )] - fn on_texture( - &mut self, - extension_data: Option<&serde_json::Map>, - texture: Handle, - ) { - } + fn on_texture(&mut self, gltf_texture: &gltf::Texture, texture: Handle) {} /// Called when an individual material is processed #[expect( diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 68e5eb41dc69d..d18324a3ba9b0 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -644,10 +644,7 @@ impl GltfLoader { image.process_loaded_texture(load_context, &mut texture_handles); // let extensions handle texture data for extension in extensions.iter_mut() { - extension.on_texture( - texture.extensions(), - texture_handles.last().unwrap().clone(), - ); + extension.on_texture(&texture, texture_handles.last().unwrap().clone()); } } } else { @@ -658,9 +655,8 @@ impl GltfLoader { let asset_path = load_context.path().clone(); let linear_textures = &linear_textures; let buffer_data = &buffer_data; - let extension_data = gltf_texture.extensions().map(ToOwned::to_owned); scope.spawn(async move { - let result = load_image( + load_image( gltf_texture, buffer_data, linear_textures, @@ -669,23 +665,19 @@ impl GltfLoader { default_sampler, settings, ) - .await; - (extension_data, result) + .await }); }); }) .into_iter() - .for_each(|(extension_data, result)| match result { + // order is preserved if the futures are only spawned from the root scope + .zip(gltf.textures()) + .for_each(|(result, texture)| match result { Ok(image) => { image.process_loaded_texture(load_context, &mut texture_handles); // let extensions handle texture data - // We do this differently here because of the IoTaskPool vs - // gltf::Texture lifetimes for extension in extensions.iter_mut() { - extension.on_texture( - extension_data.as_ref(), - texture_handles.last().unwrap().clone(), - ); + extension.on_texture(&texture, texture_handles.last().unwrap().clone()); } } Err(err) => { From 0558f321ec4d7172d98cb934eb69e1a1e5b939b0 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Sun, 30 Nov 2025 16:20:16 +0000 Subject: [PATCH 129/133] InfallibleMesh --- .../benches/bevy_render/compute_normals.rs | 4 +- crates/bevy_camera/src/primitives.rs | 3 +- crates/bevy_gltf/src/loader/mod.rs | 7 +- crates/bevy_mesh/src/mesh.rs | 1493 ++++++++--------- crates/bevy_mesh/src/mikktspace.rs | 8 +- crates/bevy_mesh/src/primitives/dim2.rs | 69 +- .../bevy_mesh/src/primitives/dim3/capsule.rs | 6 +- crates/bevy_mesh/src/primitives/dim3/cone.rs | 8 +- .../src/primitives/dim3/conical_frustum.rs | 6 +- .../bevy_mesh/src/primitives/dim3/cuboid.rs | 6 +- .../bevy_mesh/src/primitives/dim3/cylinder.rs | 6 +- crates/bevy_mesh/src/primitives/dim3/plane.rs | 6 +- .../src/primitives/dim3/polyline3d.rs | 6 +- .../src/primitives/dim3/segment3d.rs | 6 +- .../bevy_mesh/src/primitives/dim3/sphere.rs | 12 +- .../src/primitives/dim3/tetrahedron.rs | 6 +- crates/bevy_mesh/src/primitives/dim3/torus.rs | 6 +- .../src/primitives/dim3/triangle3d.rs | 6 +- crates/bevy_mesh/src/primitives/extrusion.rs | 14 +- crates/bevy_mesh/src/primitives/mod.rs | 15 +- crates/bevy_pbr/src/decal/forward.rs | 2 +- crates/bevy_pbr/src/meshlet/from_mesh.rs | 10 +- .../mesh_picking/ray_cast/intersections.rs | 15 +- crates/bevy_render/src/mesh/allocator.rs | 5 +- crates/bevy_render/src/mesh/mod.rs | 21 +- crates/bevy_solari/src/scene/blas.rs | 7 +- examples/2d/mesh2d_manual.rs | 4 +- examples/2d/mesh2d_vertex_color_texture.rs | 4 +- examples/3d/generate_custom_mesh.rs | 6 +- examples/3d/lines.rs | 8 +- examples/3d/motion_blur.rs | 3 +- examples/3d/solari.rs | 14 +- examples/3d/vertex_colors.rs | 7 +- examples/animation/custom_skinned_mesh.rs | 4 +- examples/animation/morph_targets.rs | 2 +- examples/asset/alter_mesh.rs | 2 +- examples/gltf/query_gltf_primitives.rs | 2 +- examples/math/custom_primitives.rs | 6 +- examples/math/sampling_primitives.rs | 3 +- .../custom_vertex_attribute.rs | 4 +- .../specialized_mesh_pipeline.rs | 4 +- .../tools/scene_viewer/morph_viewer_plugin.rs | 2 +- tests/3d/test_invalid_skinned_mesh.rs | 4 +- 43 files changed, 929 insertions(+), 903 deletions(-) diff --git a/benches/benches/bevy_render/compute_normals.rs b/benches/benches/bevy_render/compute_normals.rs index 74fcbfe7b11bd..0306038196e21 100644 --- a/benches/benches/bevy_render/compute_normals.rs +++ b/benches/benches/bevy_render/compute_normals.rs @@ -5,7 +5,7 @@ use rand::random; use std::time::{Duration, Instant}; use bevy_asset::RenderAssetUsages; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_mesh::{Indices, InfallibleMesh, Mesh, PrimitiveTopology}; const GRID_SIZE: usize = 256; @@ -28,7 +28,7 @@ fn compute_normals(c: &mut Criterion) { .flat_map(|i| std::iter::repeat(i).zip(0..GRID_SIZE)) .map(|(i, j)| [i as f32, j as f32, random::()]) .collect::>(); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::MAIN_WORLD, ) diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index 9d2a5203c6669..c3edf3f5f1a77 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -23,8 +23,7 @@ impl MeshAabb for Mesh { return Some(aabb.into()); } - let Ok(VertexAttributeValues::Float32x3(values)) = - self.try_attribute(Mesh::ATTRIBUTE_POSITION) + let Ok(VertexAttributeValues::Float32x3(values)) = self.attribute(Mesh::ATTRIBUTE_POSITION) else { return None; }; diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index d18324a3ba9b0..2667b7ca3b86c 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -31,7 +31,7 @@ use bevy_math::{Mat4, Vec3}; use bevy_mesh::{ morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights}, skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, Mesh, Mesh3d, MeshVertexAttribute, PrimitiveTopology, + Indices, InfallibleMesh, Mesh, Mesh3d, MeshVertexAttribute, PrimitiveTopology, }; #[cfg(feature = "pbr_transmission_textures")] use bevy_pbr::UvChannel; @@ -727,7 +727,7 @@ impl GltfLoader { }; let primitive_topology = primitive_topology(primitive.mode())?; - let mut mesh = Mesh::new(primitive_topology, settings.load_meshes); + let mut mesh = InfallibleMesh::new(primitive_topology, settings.load_meshes); // Read vertex attributes for (semantic, accessor) in primitive.attributes() { @@ -837,7 +837,8 @@ impl GltfLoader { }); } - let mesh_handle = load_context.add_labeled_asset(primitive_label.to_string(), mesh); + let mesh_handle = + load_context.add_labeled_asset(primitive_label.to_string(), mesh.into()); primitives.push(super::GltfPrimitive::new( &gltf_mesh, &primitive, diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index ca224d6e286eb..a1ce73ddeb943 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -133,6 +133,604 @@ impl From> for MeshExtractableData { } } +/// A struct which wraps a `Mesh` which is guaranteed to not be extracted to the `RenderWorld`, +/// that can be converted into a normal `Mesh` using `InfallibleMesh::into_mesh`. +/// Designed for use during cpu-side mesh construction. Methods on this struct whose `Mesh` +/// counterparts may return `MeshAccessError::ExtractedToRenderWorld` instead return plain values. +#[derive(Asset, Debug, Clone, Reflect, PartialEq)] +#[reflect(Clone)] +pub struct InfallibleMesh(Mesh); + +impl InfallibleMesh { + /// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the + /// renderer knows how to treat the vertex data. Most of the time this will be + /// [`PrimitiveTopology::TriangleList`]. + pub fn new(primitive_topology: PrimitiveTopology, asset_usage: RenderAssetUsages) -> Self { + Self(Mesh::new(primitive_topology, asset_usage)) + } + + /// Returns the topology of the mesh. + pub fn primitive_topology(&self) -> PrimitiveTopology { + self.0.primitive_topology + } + + /// Sets the data for a vertex attribute (position, normal, etc.). The name will + /// often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + /// + /// Returns an error if the mesh data has been extracted to `RenderWorld`. + /// + /// # Panics + /// Panics when the format of the values does not match the attribute's format. + pub fn insert_attribute( + &mut self, + attribute: MeshVertexAttribute, + values: impl Into, + ) { + self.0.insert_attribute(attribute, values).unwrap(); + } + + /// Consumes the mesh and returns a mesh with data set for a vertex attribute (position, normal, etc.). + /// The name will often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`]. + /// + /// (Alternatively, you can use [`InfallibleMesh::insert_attribute`] to mutate an existing mesh in-place) + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + /// + /// # Panics + /// Panics when the format of the values does not match the attribute's format. + #[must_use] + #[inline] + pub fn with_inserted_attribute( + mut self, + attribute: MeshVertexAttribute, + values: impl Into, + ) -> Self { + self.insert_attribute(attribute, values); + self + } + + /// Removes the data for a vertex attribute + pub fn remove_attribute( + &mut self, + attribute: impl Into, + ) -> Option { + match self.0.remove_attribute(attribute) { + Ok(attr) => Some(attr), + Err(MeshAccessError::NotFound) => None, + _ => unreachable!(), + } + } + + /// Consumes the mesh and returns a mesh without the data for a vertex attribute + /// + /// (Alternatively, you can use [`InfallibleMesh::remove_attribute`] to mutate an existing mesh in-place) + #[must_use] + pub fn with_removed_attribute(mut self, attribute: impl Into) -> Self { + self.remove_attribute(attribute); + self + } + + /// Returns a bool indicating if the attribute is present in this mesh's vertex data. + #[inline] + pub fn contains_attribute(&self, id: impl Into) -> bool { + self.0 + .attributes + .as_ref() + .expect(MESH_EXTRACTED_ERROR) + .contains_key(&id.into()) + } + + /// Retrieves the data currently set to the vertex attribute with the specified [`MeshVertexAttributeId`]. + #[inline] + pub fn attribute( + &self, + id: impl Into, + ) -> Option<&VertexAttributeValues> { + self.0.attribute_option(id).expect(MESH_EXTRACTED_ERROR) + } + + /// Retrieves the data currently set to the vertex attribute with the specified `name` mutably. + #[inline] + pub fn attribute_mut( + &mut self, + id: impl Into, + ) -> Option<&mut VertexAttributeValues> { + self.0.attribute_mut_option(id).expect(MESH_EXTRACTED_ERROR) + } + + /// Returns an iterator that yields references to the data of each vertex attribute. + pub fn attributes( + &self, + ) -> impl Iterator { + self.0.attributes().expect(MESH_EXTRACTED_ERROR) + } + + /// Returns an iterator that yields mutable references to the data of each vertex attribute. + pub fn attributes_mut( + &mut self, + ) -> impl Iterator { + self.0.attributes_mut().expect(MESH_EXTRACTED_ERROR) + } + + /// Sets the vertex indices of the mesh. They describe how triangles are constructed out of the + /// vertex attributes and are therefore only useful for the [`PrimitiveTopology`] variants + /// that use triangles. + #[inline] + pub fn insert_indices(&mut self, indices: Indices) { + self.0.insert_indices(indices).expect(MESH_EXTRACTED_ERROR); + } + + /// Consumes the mesh and returns a mesh with the given vertex indices. They describe how triangles + /// are constructed out of the vertex attributes and are therefore only useful for the + /// [`PrimitiveTopology`] variants that use triangles. + /// + /// (Alternatively, you can use [`InfallibleMesh::insert_indices`] to mutate an existing mesh in-place) + #[must_use] + #[inline] + pub fn with_inserted_indices(mut self, indices: Indices) -> Self { + self.insert_indices(indices); + self + } + + /// Retrieves the vertex `indices` of the mesh, returns None if not found. + #[inline] + pub fn indices(&self) -> Option<&Indices> { + self.0.indices.as_ref_option().expect(MESH_EXTRACTED_ERROR) + } + + /// Retrieves the vertex `indices` of the mesh mutably. + #[inline] + pub fn indices_mut(&mut self) -> Option<&mut Indices> { + self.0.indices_mut_option().expect(MESH_EXTRACTED_ERROR) + } + + /// Removes the vertex `indices` from the mesh and returns them. + #[inline] + pub fn remove_indices(&mut self) -> Option { + self.0.remove_indices().expect(MESH_EXTRACTED_ERROR) + } + + /// Consumes the mesh and returns a mesh without the vertex `indices` of the mesh. + /// + /// (Alternatively, you can use [`InfallibleMesh::remove_indices`] to mutate an existing mesh in-place) + #[must_use] + pub fn with_removed_indices(mut self) -> Self { + self.remove_indices(); + self + } + + pub fn duplicate_vertices(&mut self) { + self.0.duplicate_vertices().expect(MESH_EXTRACTED_ERROR); + } + + /// 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. + /// + /// (Alternatively, you can use [`InfallibleMesh::duplicate_vertices`] to mutate an existing mesh in-place) + #[must_use] + pub fn with_duplicated_vertices(self) -> Self { + Self( + self.0 + .with_duplicated_vertices() + .expect(MESH_EXTRACTED_ERROR), + ) + } + + /// 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> { + self.0.invert_winding() + } + + /// 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.0.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. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].= + pub fn compute_normals(&mut self) { + self.0.compute_normals().expect(MESH_EXTRACTED_ERROR); + } + + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. + /// + /// # Panics + /// Panics if [`Indices`] are set or [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Consider calling [`InfallibleMesh::duplicate_vertices`] or exporting your mesh with normal + /// attributes. + /// FIXME: This should handle more cases since this is called as a part of gltf + /// mesh loading where we can't really blame users for loading meshes that might + /// not conform to the limitations here! + pub fn compute_flat_normals(&mut self) { + self.0.compute_flat_normals().expect(MESH_EXTRACTED_ERROR); + } + + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared + /// vertices. + /// + /// This method weights normals by the angles of the corners of connected triangles, thus + /// eliminating triangle area and count as factors in the final normal. This does make it + /// somewhat slower than [`InfallibleMesh::compute_area_weighted_normals`] which does not need to + /// greedily normalize each triangle's normal or calculate corner angles. + /// + /// If you would rather have the computed normals be weighted by triangle area, see + /// [`InfallibleMesh::compute_area_weighted_normals`] instead. If you need to weight them in some other + /// way, see [`InfallibleMesh::compute_custom_smooth_normals`]. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle + /// this as an error use [`InfallibleMesh::compute_smooth_normals`] + pub fn compute_smooth_normals(&mut self) { + self.0.compute_smooth_normals().expect(MESH_EXTRACTED_ERROR); + } + + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared + /// vertices. + /// + /// This method weights normals by the area of each triangle containing the vertex. Thus, + /// larger triangles will skew the normals of their vertices towards their own normal more + /// than smaller triangles will. + /// + /// This method is actually somewhat faster than [`InfallibleMesh::compute_smooth_normals`] because an + /// intermediate result of triangle normal calculation is already scaled by the triangle's area. + /// + /// If you would rather have the computed normals be influenced only by the angles of connected + /// edges, see [`InfallibleMesh::compute_smooth_normals`] instead. If you need to weight them in some + /// other way, see [`InfallibleMesh::compute_custom_smooth_normals`]. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + pub fn compute_area_weighted_normals(&mut self) { + self.0 + .compute_area_weighted_normals() + .expect(MESH_EXTRACTED_ERROR); + } + + /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared + /// vertices. + /// + /// This method allows you to customize how normals are weighted via the `per_triangle` parameter, + /// which must be a function or closure that accepts 3 parameters: + /// - The indices of the three vertices of the triangle as a `[usize; 3]`. + /// - A reference to the values of the [`Mesh::ATTRIBUTE_POSITION`] of the mesh (`&[[f32; 3]]`). + /// - A mutable reference to the sums of all normals so far. + /// + /// See also the standard methods included in Bevy for calculating smooth normals: + /// - [`InfallibleMesh::compute_smooth_normals`] + /// - [`InfallibleMesh::compute_area_weighted_normals`] + /// + /// An example that would weight each connected triangle's normal equally, thus skewing normals + /// towards the planes divided into the most triangles: + /// ``` + /// # use bevy_asset::RenderAssetUsages; + /// # use bevy_mesh::{Mesh, PrimitiveTopology, Meshable, MeshBuilder}; + /// # use bevy_math::{Vec3, primitives::Cuboid}; + /// # let mut mesh = Cuboid::default().mesh().build(); + /// mesh.compute_custom_smooth_normals(|[a, b, c], positions, normals| { + /// let normal = Vec3::from(bevy_mesh::triangle_normal(positions[a], positions[b], positions[c])); + /// for idx in [a, b, c] { + /// normals[idx] += normal; + /// } + /// }); + /// ``` + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + // + // FIXME: This should handle more cases since this is called as a part of gltf + // mesh loading where we can't really blame users for loading meshes that might + // not conform to the limitations here! + // + // When fixed, also update "Panics" sections of + // - [Mesh::compute_smooth_normals] + // - [Mesh::with_computed_smooth_normals] + // - [Mesh::compute_area_weighted_normals] + // - [Mesh::with_computed_area_weighted_normals] + pub fn compute_custom_smooth_normals( + &mut self, + per_triangle: impl FnMut([usize; 3], &[[f32; 3]], &mut [Vec3]), + ) { + self.0 + .compute_custom_smooth_normals(per_triangle) + .expect(MESH_EXTRACTED_ERROR); + } + + /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. + /// If the mesh is indexed, this defaults to smooth normals. Otherwise, it defaults to flat + /// normals. + /// + /// (Alternatively, you can use [`InfallibleMesh::compute_normals`] to mutate an existing mesh in-place) + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + #[must_use] + pub fn with_computed_normals(mut self) -> Self { + self.compute_normals(); + self + } + + /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. + /// + /// (Alternatively, you can use [`InfallibleMesh::compute_flat_normals`] to mutate an existing mesh in-place) + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh has indices defined + pub fn with_computed_flat_normals(mut self) -> Self { + self.compute_flat_normals(); + self + } + + /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. + /// + /// (Alternatively, you can use [`InfallibleMesh::compute_smooth_normals`] to mutate an existing mesh in-place) + /// + /// This method weights normals by the angles of triangle corners connected to each vertex. If + /// you would rather have the computed normals be weighted by triangle area, see + /// [`InfallibleMesh::with_computed_area_weighted_normals`] instead. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + pub fn with_computed_smooth_normals(mut self) -> Self { + self.compute_smooth_normals(); + self + } + + /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. + /// + /// (Alternatively, you can use [`InfallibleMesh::compute_area_weighted_normals`] to mutate an existing mesh in-place) + /// + /// This method weights normals by the area of each triangle containing the vertex. Thus, + /// larger triangles will skew the normals of their vertices towards their own normal more + /// than smaller triangles will. If you would rather have the computed normals be influenced + /// only by the angles of connected edges, see [`InfallibleMesh::with_computed_smooth_normals`] instead. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + pub fn with_computed_area_weighted_normals(mut self) -> Self { + self.compute_area_weighted_normals(); + self + } + + /// Generate tangents for the mesh using the `mikktspace` algorithm. + /// + /// Sets the [`Mesh::ATTRIBUTE_TANGENT`] attribute if successful. + /// Requires a [`PrimitiveTopology::TriangleList`] topology and the [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes set. + #[cfg(feature = "bevy_mikktspace")] + pub fn generate_tangents(&mut self) -> Result<(), super::GenerateTangentsError> { + let tangents = super::generate_tangents_for_mesh(&self.0)?; + self.insert_attribute(Mesh::ATTRIBUTE_TANGENT, tangents); + Ok(()) + } + + /// Consumes the mesh and returns a mesh with tangents generated using the `mikktspace` algorithm. + /// + /// The resulting mesh will have the [`Mesh::ATTRIBUTE_TANGENT`] attribute if successful. + /// + /// (Alternatively, you can use [`InfallibleMesh::generate_tangents`] to mutate an existing mesh in-place) + /// + /// Requires a [`PrimitiveTopology::TriangleList`] topology and the [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes set. + #[cfg(feature = "bevy_mikktspace")] + pub fn with_generated_tangents(mut self) -> Result { + self.generate_tangents()?; + Ok(self) + } + + /// Merges the [`Mesh`] data of `other` with `self`. The attributes and indices of `other` will be appended to `self`. + /// + /// Note that attributes of `other` that don't exist on `self` will be ignored. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + /// + /// # Errors + /// + /// If any of the following conditions are not met, this function errors: + /// * All of the vertex attributes that have the same attribute id, must also + /// have the same attribute type. + /// For example two attributes with the same id, but where one is a + /// [`VertexAttributeValues::Float32`] and the other is a + /// [`VertexAttributeValues::Float32x3`], would be invalid. + /// * Both meshes must have the same primitive topology. + pub fn merge(&mut self, other: &Mesh) -> Result<(), MeshMergeError> { + self.0.merge(other) + } + + /// Transforms the vertex positions, normals, and tangents of the mesh by the given [`Transform`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn transformed_by(mut self, transform: Transform) -> Self { + self.transform_by(transform); + self + } + + /// Transforms the vertex positions, normals, and tangents of the mesh in place by the given [`Transform`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn transform_by(&mut self, transform: Transform) { + self.0.transform_by(transform).expect(MESH_EXTRACTED_ERROR); + } + + /// Translates the vertex positions of the mesh by the given [`Vec3`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn translated_by(mut self, translation: Vec3) -> Self { + self.translate_by(translation); + self + } + + /// Translates the vertex positions of the mesh in place by the given [`Vec3`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn translate_by(&mut self, translation: Vec3) { + self.0 + .translate_by(translation) + .expect(MESH_EXTRACTED_ERROR); + } + + /// Rotates the vertex positions, normals, and tangents of the mesh by the given [`Quat`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn rotated_by(mut self, rotation: Quat) -> Self { + self.rotate_by(rotation); + self + } + + /// Rotates the vertex positions, normals, and tangents of the mesh in place by the given [`Quat`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn rotate_by(&mut self, rotation: Quat) { + self.0.rotate_by(rotation).expect(MESH_EXTRACTED_ERROR); + } + + /// Scales the vertex positions, normals, and tangents of the mesh by the given [`Vec3`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn scaled_by(mut self, scale: Vec3) -> Self { + self.scale_by(scale); + self + } + + /// Scales the vertex positions, normals, and tangents of the mesh in place by the given [`Vec3`]. + /// + /// `Aabb` of entities with modified mesh are not updated automatically. + pub fn scale_by(&mut self, scale: Vec3) { + self.0.scale_by(scale).expect(MESH_EXTRACTED_ERROR); + } + + /// Normalize joint weights so they sum to 1. + pub fn normalize_joint_weights(&mut self) { + self.0 + .normalize_joint_weights() + .expect(MESH_EXTRACTED_ERROR); + } + + /// Get a list of this Mesh's [triangles] as an iterator if possible. + /// + /// Returns an error if any of the following conditions are met (see [`MeshTrianglesError`]): + /// * The Mesh's [primitive topology] is not `TriangleList` or `TriangleStrip`. + /// * The Mesh is missing position or index data. + /// * The Mesh's position data has the wrong format (not `Float32x3`). + /// + /// [primitive topology]: PrimitiveTopology + /// [triangles]: Triangle3d + pub fn triangles(&self) -> Result + '_, MeshTrianglesError> { + self.0.triangles() + } + + pub fn into_mesh(self) -> Mesh { + self.0 + } +} + +impl From for Mesh { + fn from(value: InfallibleMesh) -> Self { + value.into_mesh() + } +} + +#[cfg(feature = "morph")] +impl InfallibleMesh { + /// Whether this mesh has morph targets. + pub fn has_morph_targets(&self) -> bool { + self.0.has_morph_targets().expect(MESH_EXTRACTED_ERROR) + } + + /// Set [morph targets] image for this mesh. This requires a "morph target image". See [`MorphTargetImage`](crate::morph::MorphTargetImage) for info. + /// + /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation + pub fn set_morph_targets(&mut self, morph_targets: Handle) { + self.0 + .set_morph_targets(morph_targets) + .expect(MESH_EXTRACTED_ERROR); + } + + /// Retrieve the morph targets for this mesh, or None if there are no morph targets. + pub fn morph_targets(&self) -> Option<&Handle> { + match self.0.morph_targets() { + Ok(targets) => Some(targets), + Err(MeshAccessError::NotFound) => None, + Err(MeshAccessError::ExtractedToRenderWorld) => panic!("{MESH_EXTRACTED_ERROR}"), + } + } + + /// Consumes the mesh and returns a mesh with the given [morph targets]. + /// + /// This requires a "morph target image". See [`MorphTargetImage`](crate::morph::MorphTargetImage) for info. + /// + /// (Alternatively, you can use [`InfallibleMesh::set_morph_targets`] to mutate an existing mesh in-place) + /// + /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation + #[must_use] + pub fn with_morph_targets(mut self, morph_targets: Handle) -> Self { + self.set_morph_targets(morph_targets); + self + } + + /// Sets the names of each morph target. This should correspond to the order of the morph targets in `set_morph_targets`. + pub fn set_morph_target_names(&mut self, names: Vec) { + self.0 + .set_morph_target_names(names) + .expect(MESH_EXTRACTED_ERROR); + } + + /// Consumes the mesh and returns a mesh with morph target names. + /// Names should correspond to the order of the morph targets in `set_morph_targets`. + /// + /// (Alternatively, you can use [`InfallibleMesh::set_morph_target_names`] to mutate an existing mesh in-place) + #[must_use] + pub fn with_morph_target_names(self, names: Vec) -> Self { + Self( + self.0 + .with_morph_target_names(names) + .expect(MESH_EXTRACTED_ERROR), + ) + } + + /// Gets a list of all morph target names, if they exist. + pub fn morph_target_names(&self) -> Option<&[String]> { + self.0.morph_target_names().expect(MESH_EXTRACTED_ERROR) + } +} + +impl core::ops::Deref for InfallibleMesh { + type Target = Mesh; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// A 3D object made out of vertices representing triangles, lines, or points, /// with "attribute" values for each vertex. /// @@ -357,25 +955,6 @@ impl Mesh { self.primitive_topology } - /// Sets the data for a vertex attribute (position, normal, etc.). The name will - /// often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the format of the values does not match the attribute's format. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_insert_attribute`] - #[inline] - pub fn insert_attribute( - &mut self, - attribute: MeshVertexAttribute, - values: impl Into, - ) { - self.try_insert_attribute(attribute, values) - .expect(MESH_EXTRACTED_ERROR); - } - /// Sets the data for a vertex attribute (position, normal, etc.). The name will /// often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`]. /// @@ -386,7 +965,7 @@ impl Mesh { /// # Panics /// Panics when the format of the values does not match the attribute's format. #[inline] - pub fn try_insert_attribute( + pub fn insert_attribute( &mut self, attribute: MeshVertexAttribute, values: impl Into, @@ -406,28 +985,6 @@ impl Mesh { Ok(()) } - /// Consumes the mesh and returns a mesh with data set for a vertex attribute (position, normal, etc.). - /// The name will often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`]. - /// - /// (Alternatively, you can use [`Mesh::insert_attribute`] to mutate an existing mesh in-place) - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the format of the values does not match the attribute's format. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_inserted_attribute`] - #[must_use] - #[inline] - pub fn with_inserted_attribute( - mut self, - attribute: MeshVertexAttribute, - values: impl Into, - ) -> Self { - self.insert_attribute(attribute, values); - self - } - /// Consumes the mesh and returns a mesh with data set for a vertex attribute (position, normal, etc.). /// The name will often be one of the associated constants such as [`Mesh::ATTRIBUTE_POSITION`]. /// @@ -437,35 +994,19 @@ impl Mesh { /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_with_inserted_attribute( + pub fn with_inserted_attribute( mut self, attribute: MeshVertexAttribute, values: impl Into, ) -> Result { - self.try_insert_attribute(attribute, values)?; + self.insert_attribute(attribute, values)?; Ok(self) } - /// Removes the data for a vertex attribute - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_remove_attribute`] - pub fn remove_attribute( - &mut self, - attribute: impl Into, - ) -> Option { - self.attributes - .as_mut() - .expect(MESH_EXTRACTED_ERROR) - .remove(&attribute.into()) - .map(|data| data.values) - } - /// Removes the data for a vertex attribute /// Returns an error if the mesh data has been extracted to `RenderWorld`or /// if the attribute does not exist. - pub fn try_remove_attribute( + pub fn remove_attribute( &mut self, attribute: impl Into, ) -> Result { @@ -477,88 +1018,48 @@ impl Mesh { .values) } - /// Consumes the mesh and returns a mesh without the data for a vertex attribute - /// - /// (Alternatively, you can use [`Mesh::remove_attribute`] to mutate an existing mesh in-place) - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_removed_attribute`] - #[must_use] - pub fn with_removed_attribute(mut self, attribute: impl Into) -> Self { - self.remove_attribute(attribute); - self - } - /// Consumes the mesh and returns a mesh without the data for a vertex attribute /// /// (Alternatively, you can use [`Mesh::remove_attribute`] to mutate an existing mesh in-place) /// /// Returns an error if the mesh data has been extracted to `RenderWorld`or /// if the attribute does not exist. - pub fn try_with_removed_attribute( + pub fn with_removed_attribute( mut self, attribute: impl Into, ) -> Result { - self.try_remove_attribute(attribute)?; + self.remove_attribute(attribute)?; Ok(self) } - /// Returns a bool indicating if the attribute is present in this mesh's vertex data. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_contains_attribute`] - #[inline] - pub fn contains_attribute(&self, id: impl Into) -> bool { - self.attributes - .as_ref() - .expect(MESH_EXTRACTED_ERROR) - .contains_key(&id.into()) - } - /// Returns a bool indicating if the attribute is present in this mesh's vertex data. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_contains_attribute( + pub fn contains_attribute( &self, id: impl Into, ) -> Result { Ok(self.attributes.as_ref()?.contains_key(&id.into())) } - /// Retrieves the data currently set to the vertex attribute with the specified [`MeshVertexAttributeId`]. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_attribute`] or [`Mesh::try_attribute_option`] - #[inline] - pub fn attribute( - &self, - id: impl Into, - ) -> Option<&VertexAttributeValues> { - self.try_attribute_option(id).expect(MESH_EXTRACTED_ERROR) - } - /// Retrieves the data currently set to the vertex attribute with the specified [`MeshVertexAttributeId`]. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`or /// if the attribute does not exist. #[inline] - pub fn try_attribute( + pub fn attribute( &self, id: impl Into, ) -> Result<&VertexAttributeValues, MeshAccessError> { - self.try_attribute_option(id)? - .ok_or(MeshAccessError::NotFound) + self.attribute_option(id)?.ok_or(MeshAccessError::NotFound) } /// Retrieves the data currently set to the vertex attribute with the specified [`MeshVertexAttributeId`]. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_attribute_option( + pub fn attribute_option( &self, id: impl Into, ) -> Result, MeshAccessError> { @@ -571,37 +1072,23 @@ impl Mesh { /// Retrieves the full data currently set to the vertex attribute with the specified [`MeshVertexAttributeId`]. #[inline] - pub(crate) fn try_attribute_data( + pub(crate) fn attribute_data( &self, id: impl Into, ) -> Result, MeshAccessError> { Ok(self.attributes.as_ref()?.get(&id.into())) } - /// Retrieves the data currently set to the vertex attribute with the specified `name` mutably. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_attribute_mut`] - #[inline] - pub fn attribute_mut( - &mut self, - id: impl Into, - ) -> Option<&mut VertexAttributeValues> { - self.try_attribute_mut_option(id) - .expect(MESH_EXTRACTED_ERROR) - } - /// Retrieves the data currently set to the vertex attribute with the specified `name` mutably. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`or /// if the attribute does not exist. #[inline] - pub fn try_attribute_mut( + pub fn attribute_mut( &mut self, id: impl Into, ) -> Result<&mut VertexAttributeValues, MeshAccessError> { - self.try_attribute_mut_option(id)? + self.attribute_mut_option(id)? .ok_or(MeshAccessError::NotFound) } @@ -609,7 +1096,7 @@ impl Mesh { /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_attribute_mut_option( + pub fn attribute_mut_option( &mut self, id: impl Into, ) -> Result, MeshAccessError> { @@ -620,20 +1107,9 @@ impl Mesh { .map(|data| &mut data.values)) } - /// Returns an iterator that yields references to the data of each vertex attribute. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_attributes`] - pub fn attributes( - &self, - ) -> impl Iterator { - self.try_attributes().expect(MESH_EXTRACTED_ERROR) - } - /// Returns an iterator that yields references to the data of each vertex attribute. /// Returns an error if data has been extracted to `RenderWorld` - pub fn try_attributes( + pub fn attributes( &self, ) -> Result, MeshAccessError> { @@ -644,21 +1120,10 @@ impl Mesh { .map(|data| (&data.attribute, &data.values))) } - /// Returns an iterator that yields mutable references to the data of each vertex attribute. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_attributes_mut`] - pub fn attributes_mut( - &mut self, - ) -> impl Iterator { - self.try_attributes_mut().expect(MESH_EXTRACTED_ERROR) - } - /// Returns an iterator that yields mutable references to the data of each vertex attribute. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_attributes_mut( + pub fn attributes_mut( &mut self, ) -> Result< impl Iterator, @@ -671,27 +1136,13 @@ impl Mesh { .map(|data| (&data.attribute, &mut data.values))) } - /// Sets the vertex indices of the mesh. They describe how triangles are constructed out of the - /// vertex attributes and are therefore only useful for the [`PrimitiveTopology`] variants - /// that use triangles. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_insert_indices`] - #[inline] - pub fn insert_indices(&mut self, indices: Indices) { - self.indices - .replace(Some(indices)) - .expect(MESH_EXTRACTED_ERROR); - } - /// Sets the vertex indices of the mesh. They describe how triangles are constructed out of the /// vertex attributes and are therefore only useful for the [`PrimitiveTopology`] variants /// that use triangles. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_insert_indices(&mut self, indices: Indices) -> Result<(), MeshAccessError> { + pub fn insert_indices(&mut self, indices: Indices) -> Result<(), MeshAccessError> { self.indices.replace(Some(indices))?; Ok(()) } @@ -702,45 +1153,19 @@ impl Mesh { /// /// (Alternatively, you can use [`Mesh::insert_indices`] to mutate an existing mesh in-place) /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_inserted_indices`] - #[must_use] - #[inline] - pub fn with_inserted_indices(mut self, indices: Indices) -> Self { - self.insert_indices(indices); - self - } - - /// Consumes the mesh and returns a mesh with the given vertex indices. They describe how triangles - /// are constructed out of the vertex attributes and are therefore only useful for the - /// [`PrimitiveTopology`] variants that use triangles. - /// - /// (Alternatively, you can use [`Mesh::try_insert_indices`] to mutate an existing mesh in-place) - /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_with_inserted_indices(mut self, indices: Indices) -> Result { - self.try_insert_indices(indices)?; + pub fn with_inserted_indices(mut self, indices: Indices) -> Result { + self.insert_indices(indices)?; Ok(self) } - /// Retrieves the vertex `indices` of the mesh, returns None if not found. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_indices`] - #[inline] - pub fn indices(&self) -> Option<&Indices> { - self.indices.as_ref_option().expect(MESH_EXTRACTED_ERROR) - } - /// Retrieves the vertex `indices` of the mesh. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`or /// if the attribute does not exist. #[inline] - pub fn try_indices(&self) -> Result<&Indices, MeshAccessError> { + pub fn indices(&self) -> Result<&Indices, MeshAccessError> { self.indices.as_ref() } @@ -748,21 +1173,15 @@ impl Mesh { /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_indices_option(&self) -> Result, MeshAccessError> { + pub fn indices_option(&self) -> Result, MeshAccessError> { self.indices.as_ref_option() } - /// Retrieves the vertex `indices` of the mesh mutably. - #[inline] - pub fn indices_mut(&mut self) -> Option<&mut Indices> { - self.try_indices_mut_option().expect(MESH_EXTRACTED_ERROR) - } - /// Retrieves the vertex `indices` of the mesh mutably. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_indices_mut(&mut self) -> Result<&mut Indices, MeshAccessError> { + pub fn indices_mut(&mut self) -> Result<&mut Indices, MeshAccessError> { self.indices.as_mut() } @@ -770,25 +1189,15 @@ impl Mesh { /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_indices_mut_option(&mut self) -> Result, MeshAccessError> { + pub fn indices_mut_option(&mut self) -> Result, MeshAccessError> { self.indices.as_mut_option() } - /// Removes the vertex `indices` from the mesh and returns them. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_remove_indices`] - #[inline] - pub fn remove_indices(&mut self) -> Option { - self.try_remove_indices().expect(MESH_EXTRACTED_ERROR) - } - /// Removes the vertex `indices` from the mesh and returns them. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. #[inline] - pub fn try_remove_indices(&mut self) -> Result, MeshAccessError> { + pub fn remove_indices(&mut self) -> Result, MeshAccessError> { self.indices.replace(None) } @@ -796,22 +1205,9 @@ impl Mesh { /// /// (Alternatively, you can use [`Mesh::remove_indices`] to mutate an existing mesh in-place) /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_removed_indices`] - #[must_use] - pub fn with_removed_indices(mut self) -> Self { - self.remove_indices(); - self - } - - /// Consumes the mesh and returns a mesh without the vertex `indices` of the mesh. - /// - /// (Alternatively, you can use [`Mesh::try_remove_indices`] to mutate an existing mesh in-place) - /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_with_removed_indices(mut self) -> Result { - self.try_remove_indices()?; + pub fn with_removed_indices(mut self) -> Result { + self.remove_indices()?; Ok(self) } @@ -967,25 +1363,13 @@ impl Mesh { } } - /// Duplicates the vertex attributes so that no vertices are shared. - /// - /// This can dramatically increase the vertex count, so make sure this is what you want. - /// Does nothing if no [Indices] are set. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_duplicate_vertices`] - pub fn duplicate_vertices(&mut self) { - self.try_duplicate_vertices().expect(MESH_EXTRACTED_ERROR); - } - /// Duplicates the vertex attributes so that no vertices are shared. /// /// This can dramatically increase the vertex count, so make sure this is what you want. /// Does nothing if no [Indices] are set. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_duplicate_vertices(&mut self) -> Result<(), MeshAccessError> { + pub fn duplicate_vertices(&mut self) -> Result<(), MeshAccessError> { fn duplicate(values: &[T], indices: impl Iterator) -> Vec { indices.map(|i| values[i]).collect() } @@ -1044,25 +1428,9 @@ impl Mesh { /// /// (Alternatively, you can use [`Mesh::duplicate_vertices`] to mutate an existing mesh in-place) /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_duplicated_vertices`] - #[must_use] - pub fn with_duplicated_vertices(mut self) -> Self { - self.duplicate_vertices(); - self - } - - /// 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. - /// - /// (Alternatively, you can use [`Mesh::try_duplicate_vertices`] to mutate an existing mesh in-place) - /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_with_duplicated_vertices(mut self) -> Result { - self.try_duplicate_vertices()?; + pub fn with_duplicated_vertices(mut self) -> Result { + self.duplicate_vertices()?; Ok(self) } @@ -1132,48 +1500,18 @@ impl Mesh { /// # Panics /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].= - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_compute_normals`] - pub fn compute_normals(&mut self) { - self.try_compute_normals().expect(MESH_EXTRACTED_ERROR); - } - - /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. - /// If the mesh is indexed, this defaults to smooth normals. Otherwise, it defaults to flat - /// normals. - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].= - pub fn try_compute_normals(&mut self) -> Result<(), MeshAccessError> { + pub fn compute_normals(&mut self) -> Result<(), MeshAccessError> { assert!( matches!(self.primitive_topology, PrimitiveTopology::TriangleList), "`compute_normals` can only work on `TriangleList`s" ); - if self.try_indices_option()?.is_none() { - self.try_compute_flat_normals() + if self.indices_option()?.is_none() { + self.compute_flat_normals() } else { - self.try_compute_smooth_normals() + self.compute_smooth_normals() } } - /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. - /// - /// # Panics - /// Panics if [`Indices`] are set or [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Consider calling [`Mesh::duplicate_vertices`] or exporting your mesh with normal - /// attributes. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_compute_flat_normals`] - /// - /// FIXME: This should handle more cases since this is called as a part of gltf - /// mesh loading where we can't really blame users for loading meshes that might - /// not conform to the limitations here! - pub fn compute_flat_normals(&mut self) { - self.try_compute_flat_normals().expect(MESH_EXTRACTED_ERROR); - } - /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of a mesh. /// /// # Panics @@ -1185,9 +1523,9 @@ impl Mesh { /// FIXME: This should handle more cases since this is called as a part of gltf /// mesh loading where we can't really blame users for loading meshes that might /// not conform to the limitations here! - pub fn try_compute_flat_normals(&mut self) -> Result<(), MeshAccessError> { + pub fn compute_flat_normals(&mut self) -> Result<(), MeshAccessError> { assert!( - self.try_indices_option()?.is_none(), + self.indices_option()?.is_none(), "`compute_flat_normals` can't work on indexed geometry. Consider calling either `Mesh::compute_smooth_normals` or `Mesh::duplicate_vertices` followed by `Mesh::compute_flat_normals`." ); assert!( @@ -1196,7 +1534,7 @@ impl Mesh { ); let positions = self - .try_attribute(Mesh::ATTRIBUTE_POSITION)? + .attribute(Mesh::ATTRIBUTE_POSITION)? .as_float3() .expect("`Mesh::ATTRIBUTE_POSITION` vertex attributes should be of type `float3`"); @@ -1206,30 +1544,7 @@ impl Mesh { .flat_map(|normal| [normal; 3]) .collect(); - self.try_insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - } - - /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared - /// vertices. - /// - /// This method weights normals by the angles of the corners of connected triangles, thus - /// eliminating triangle area and count as factors in the final normal. This does make it - /// somewhat slower than [`Mesh::compute_area_weighted_normals`] which does not need to - /// greedily normalize each triangle's normal or calculate corner angles. - /// - /// If you would rather have the computed normals be weighted by triangle area, see - /// [`Mesh::compute_area_weighted_normals`] instead. If you need to weight them in some other - /// way, see [`Mesh::compute_custom_smooth_normals`]. - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh does not have indices defined. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_compute_smooth_normals`] - pub fn compute_smooth_normals(&mut self) { - self.try_compute_smooth_normals() - .expect(MESH_EXTRACTED_ERROR); + self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals) } /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared @@ -1248,8 +1563,8 @@ impl Mesh { /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. /// Panics if the mesh does not have indices defined. - pub fn try_compute_smooth_normals(&mut self) -> Result<(), MeshAccessError> { - self.try_compute_custom_smooth_normals(|[a, b, c], positions, normals| { + pub fn compute_smooth_normals(&mut self) -> Result<(), MeshAccessError> { + self.compute_custom_smooth_normals(|[a, b, c], positions, normals| { let pa = Vec3::from(positions[a]); let pb = Vec3::from(positions[b]); let pc = Vec3::from(positions[c]); @@ -1291,107 +1606,30 @@ impl Mesh { /// /// This method weights normals by the area of each triangle containing the vertex. Thus, /// larger triangles will skew the normals of their vertices towards their own normal more - /// than smaller triangles will. - /// - /// This method is actually somewhat faster than [`Mesh::compute_smooth_normals`] because an - /// intermediate result of triangle normal calculation is already scaled by the triangle's area. - /// - /// If you would rather have the computed normals be influenced only by the angles of connected - /// edges, see [`Mesh::compute_smooth_normals`] instead. If you need to weight them in some - /// other way, see [`Mesh::compute_custom_smooth_normals`]. - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh does not have indices defined. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_compute_area_weighted_normals`] - pub fn compute_area_weighted_normals(&mut self) { - self.try_compute_area_weighted_normals() - .expect(MESH_EXTRACTED_ERROR); - } - - /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared - /// vertices. - /// - /// This method weights normals by the area of each triangle containing the vertex. Thus, - /// larger triangles will skew the normals of their vertices towards their own normal more - /// than smaller triangles will. - /// - /// This method is actually somewhat faster than [`Mesh::compute_smooth_normals`] because an - /// intermediate result of triangle normal calculation is already scaled by the triangle's area. - /// - /// If you would rather have the computed normals be influenced only by the angles of connected - /// edges, see [`Mesh::compute_smooth_normals`] instead. If you need to weight them in some - /// other way, see [`Mesh::compute_custom_smooth_normals`]. - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh does not have indices defined. - pub fn try_compute_area_weighted_normals(&mut self) -> Result<(), MeshAccessError> { - self.try_compute_custom_smooth_normals(|[a, b, c], positions, normals| { - let normal = Vec3::from(triangle_area_normal( - positions[a], - positions[b], - positions[c], - )); - [a, b, c].into_iter().for_each(|pos| { - normals[pos] += normal; - }); - }) - } - - /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared - /// vertices. - /// - /// This method allows you to customize how normals are weighted via the `per_triangle` parameter, - /// which must be a function or closure that accepts 3 parameters: - /// - The indices of the three vertices of the triangle as a `[usize; 3]`. - /// - A reference to the values of the [`Mesh::ATTRIBUTE_POSITION`] of the mesh (`&[[f32; 3]]`). - /// - A mutable reference to the sums of all normals so far. - /// - /// See also the standard methods included in Bevy for calculating smooth normals: - /// - [`Mesh::compute_smooth_normals`] - /// - [`Mesh::compute_area_weighted_normals`] - /// - /// An example that would weight each connected triangle's normal equally, thus skewing normals - /// towards the planes divided into the most triangles: - /// ``` - /// # use bevy_asset::RenderAssetUsages; - /// # use bevy_mesh::{Mesh, PrimitiveTopology, Meshable, MeshBuilder}; - /// # use bevy_math::{Vec3, primitives::Cuboid}; - /// # let mut mesh = Cuboid::default().mesh().build(); - /// mesh.compute_custom_smooth_normals(|[a, b, c], positions, normals| { - /// let normal = Vec3::from(bevy_mesh::triangle_normal(positions[a], positions[b], positions[c])); - /// for idx in [a, b, c] { - /// normals[idx] += normal; - /// } - /// }); - /// ``` - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh does not have indices defined. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_compute_custom_smooth_normals`] - // - // FIXME: This should handle more cases since this is called as a part of gltf - // mesh loading where we can't really blame users for loading meshes that might - // not conform to the limitations here! - // - // When fixed, also update "Panics" sections of - // - [Mesh::compute_smooth_normals] - // - [Mesh::with_computed_smooth_normals] - // - [Mesh::compute_area_weighted_normals] - // - [Mesh::with_computed_area_weighted_normals] - pub fn compute_custom_smooth_normals( - &mut self, - per_triangle: impl FnMut([usize; 3], &[[f32; 3]], &mut [Vec3]), - ) { - self.try_compute_custom_smooth_normals(per_triangle) - .expect(MESH_EXTRACTED_ERROR); + /// than smaller triangles will. + /// + /// This method is actually somewhat faster than [`Mesh::compute_smooth_normals`] because an + /// intermediate result of triangle normal calculation is already scaled by the triangle's area. + /// + /// If you would rather have the computed normals be influenced only by the angles of connected + /// edges, see [`Mesh::compute_smooth_normals`] instead. If you need to weight them in some + /// other way, see [`Mesh::compute_custom_smooth_normals`]. + /// + /// # Panics + /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. + /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. + /// Panics if the mesh does not have indices defined. + pub fn compute_area_weighted_normals(&mut self) -> Result<(), MeshAccessError> { + self.compute_custom_smooth_normals(|[a, b, c], positions, normals| { + let normal = Vec3::from(triangle_area_normal( + positions[a], + positions[b], + positions[c], + )); + [a, b, c].into_iter().for_each(|pos| { + normals[pos] += normal; + }); + }) } /// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared @@ -1436,7 +1674,7 @@ impl Mesh { // - [Mesh::with_computed_smooth_normals] // - [Mesh::compute_area_weighted_normals] // - [Mesh::with_computed_area_weighted_normals] - pub fn try_compute_custom_smooth_normals( + pub fn compute_custom_smooth_normals( &mut self, mut per_triangle: impl FnMut([usize; 3], &[[f32; 3]], &mut [Vec3]), ) -> Result<(), MeshAccessError> { @@ -1445,18 +1683,18 @@ impl Mesh { "smooth normals can only be computed on `TriangleList`s" ); assert!( - self.try_indices_option()?.is_some(), + self.indices_option()?.is_some(), "smooth normals can only be computed on indexed meshes" ); let positions = self - .try_attribute(Mesh::ATTRIBUTE_POSITION)? + .attribute(Mesh::ATTRIBUTE_POSITION)? .as_float3() .expect("`Mesh::ATTRIBUTE_POSITION` vertex attributes should be of type `float3`"); let mut normals = vec![Vec3::ZERO; positions.len()]; - self.try_indices()? + self.indices()? .iter() .collect::>() .chunks_exact(3) @@ -1466,24 +1704,7 @@ impl Mesh { *normal = normal.try_normalize().unwrap_or(Vec3::ZERO); } - self.try_insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals) - } - - /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. - /// If the mesh is indexed, this defaults to smooth normals. Otherwise, it defaults to flat - /// normals. - /// - /// (Alternatively, you can use [`Mesh::compute_normals`] to mutate an existing mesh in-place) - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_computed_normals`] - #[must_use] - pub fn with_computed_normals(self) -> Self { - self.try_with_computed_normals() - .expect(MESH_EXTRACTED_ERROR) + self.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals) } /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. @@ -1495,8 +1716,8 @@ impl Mesh { /// # Panics /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - pub fn try_with_computed_normals(mut self) -> Result { - self.try_compute_normals()?; + pub fn with_computed_normals(mut self) -> Result { + self.compute_normals()?; Ok(self) } @@ -1508,23 +1729,8 @@ impl Mesh { /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. /// Panics if the mesh has indices defined - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_computed_flat_normals`] - pub fn with_computed_flat_normals(mut self) -> Self { - self.compute_flat_normals(); - self - } - - /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. - /// - /// (Alternatively, you can use [`Mesh::compute_flat_normals`] to mutate an existing mesh in-place) - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh has indices defined - pub fn try_with_computed_flat_normals(mut self) -> Result { - self.try_compute_flat_normals()?; + pub fn with_computed_flat_normals(mut self) -> Result { + self.compute_flat_normals()?; Ok(self) } @@ -1540,26 +1746,8 @@ impl Mesh { /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. /// Panics if the mesh does not have indices defined. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_computed_smooth_normals`] - pub fn with_computed_smooth_normals(mut self) -> Self { - self.compute_smooth_normals(); - self - } - /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. - /// - /// (Alternatively, you can use [`Mesh::compute_smooth_normals`] to mutate an existing mesh in-place) - /// - /// This method weights normals by the angles of triangle corners connected to each vertex. If - /// you would rather have the computed normals be weighted by triangle area, see - /// [`Mesh::with_computed_area_weighted_normals`] instead. - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh does not have indices defined. - pub fn try_with_computed_smooth_normals(mut self) -> Result { - self.try_compute_smooth_normals()?; + pub fn with_computed_smooth_normals(mut self) -> Result { + self.compute_smooth_normals()?; Ok(self) } @@ -1576,28 +1764,8 @@ impl Mesh { /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. /// Panics if the mesh does not have indices defined. - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_computed_area_weighted_normals`] - pub fn with_computed_area_weighted_normals(mut self) -> Self { - self.compute_area_weighted_normals(); - self - } - - /// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`]. - /// - /// (Alternatively, you can use [`Mesh::compute_area_weighted_normals`] to mutate an existing mesh in-place) - /// - /// This method weights normals by the area of each triangle containing the vertex. Thus, - /// larger triangles will skew the normals of their vertices towards their own normal more - /// than smaller triangles will. If you would rather have the computed normals be influenced - /// only by the angles of connected edges, see [`Mesh::with_computed_smooth_normals`] instead. - /// - /// # Panics - /// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`. - /// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`]. - /// Panics if the mesh does not have indices defined. - pub fn try_with_computed_area_weighted_normals(mut self) -> Result { - self.try_compute_area_weighted_normals()?; + pub fn with_computed_area_weighted_normals(mut self) -> Result { + self.compute_area_weighted_normals()?; Ok(self) } @@ -1608,7 +1776,7 @@ impl Mesh { #[cfg(feature = "bevy_mikktspace")] pub fn generate_tangents(&mut self) -> Result<(), super::GenerateTangentsError> { let tangents = super::generate_tangents_for_mesh(self)?; - self.try_insert_attribute(Mesh::ATTRIBUTE_TANGENT, tangents)?; + self.insert_attribute(Mesh::ATTRIBUTE_TANGENT, tangents)?; Ok(()) } @@ -1657,8 +1825,8 @@ impl Mesh { let index_offset = self.count_vertices(); // Extend attributes of `self` with attributes of `other`. - for (attribute, values) in self.try_attributes_mut()? { - if let Some(other_values) = other.try_attribute_option(attribute.id)? { + for (attribute, values) in self.attributes_mut()? { + if let Some(other_values) = other.attribute_option(attribute.id)? { #[expect( clippy::match_same_arms, reason = "Although the bindings on some match arms may have different types, each variant has different semantics; thus it's not guaranteed that they will use the same type forever." @@ -1696,7 +1864,7 @@ impl Mesh { return Err(MeshMergeError::IncompatibleVertexAttributes { self_attribute: *attribute, other_attribute: other - .try_attribute_data(attribute.id)? + .attribute_data(attribute.id)? .map(|data| data.attribute), }) } @@ -1706,7 +1874,7 @@ impl Mesh { // Extend indices of `self` with indices of `other`. if let (Some(indices), Some(other_indices)) = - (self.try_indices_mut_option()?, other.try_indices_option()?) + (self.indices_mut_option()?, other.indices_option()?) { indices.extend(other_indices.iter().map(|i| (i + index_offset) as u32)); } @@ -1716,39 +1884,15 @@ impl Mesh { /// Transforms the vertex positions, normals, and tangents of the mesh by the given [`Transform`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_transformed_by`] - pub fn transformed_by(mut self, transform: Transform) -> Self { - self.transform_by(transform); - self - } - - /// Transforms the vertex positions, normals, and tangents of the mesh by the given [`Transform`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_transformed_by(mut self, transform: Transform) -> Result { - self.try_transform_by(transform)?; + pub fn transformed_by(mut self, transform: Transform) -> Result { + self.transform_by(transform)?; Ok(self) } /// Transforms the vertex positions, normals, and tangents of the mesh in place by the given [`Transform`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_transform_by`] - pub fn transform_by(&mut self, transform: Transform) { - self.try_transform_by(transform) - .expect(MESH_EXTRACTED_ERROR); - } - - /// Transforms the vertex positions, normals, and tangents of the mesh in place by the given [`Transform`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_transform_by(&mut self, transform: Transform) -> Result<(), MeshAccessError> { + pub fn transform_by(&mut self, transform: Transform) -> Result<(), MeshAccessError> { // Needed when transforming normals and tangents let scale_recip = 1. / transform.scale; debug_assert!( @@ -1757,7 +1901,7 @@ impl Mesh { ); if let Some(VertexAttributeValues::Float32x3(positions)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? + self.attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? { // Apply scale, rotation, and translation to vertex positions positions @@ -1774,7 +1918,7 @@ impl Mesh { } if let Some(VertexAttributeValues::Float32x3(normals)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_NORMAL)? + self.attribute_mut_option(Mesh::ATTRIBUTE_NORMAL)? { // Transform normals, taking into account non-uniform scaling and rotation normals.iter_mut().for_each(|normal| { @@ -1785,7 +1929,7 @@ impl Mesh { } if let Some(VertexAttributeValues::Float32x4(tangents)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_TANGENT)? + self.attribute_mut_option(Mesh::ATTRIBUTE_TANGENT)? { // Transform tangents, taking into account non-uniform scaling and rotation tangents.iter_mut().for_each(|tangent| { @@ -1803,45 +1947,21 @@ impl Mesh { /// Translates the vertex positions of the mesh by the given [`Vec3`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_translated_by`] - pub fn translated_by(mut self, translation: Vec3) -> Self { - self.translate_by(translation); - self - } - - /// Translates the vertex positions of the mesh by the given [`Vec3`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_translated_by(mut self, translation: Vec3) -> Result { - self.try_translate_by(translation)?; + pub fn translated_by(mut self, translation: Vec3) -> Result { + self.translate_by(translation)?; Ok(self) } /// Translates the vertex positions of the mesh in place by the given [`Vec3`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_translate_by`] - pub fn translate_by(&mut self, translation: Vec3) { - self.try_translate_by(translation) - .expect(MESH_EXTRACTED_ERROR); - } - - /// Translates the vertex positions of the mesh in place by the given [`Vec3`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_translate_by(&mut self, translation: Vec3) -> Result<(), MeshAccessError> { + pub fn translate_by(&mut self, translation: Vec3) -> Result<(), MeshAccessError> { if translation == Vec3::ZERO { return Ok(()); } if let Some(VertexAttributeValues::Float32x3(positions)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? + self.attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? { // Apply translation to vertex positions positions @@ -1855,40 +1975,17 @@ impl Mesh { /// Rotates the vertex positions, normals, and tangents of the mesh by the given [`Quat`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_rotated_by`] - pub fn rotated_by(mut self, rotation: Quat) -> Self { - self.try_rotate_by(rotation).expect(MESH_EXTRACTED_ERROR); - self - } - - /// Rotates the vertex positions, normals, and tangents of the mesh by the given [`Quat`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_rotated_by(mut self, rotation: Quat) -> Result { - self.try_rotate_by(rotation)?; + pub fn rotated_by(mut self, rotation: Quat) -> Result { + self.rotate_by(rotation)?; Ok(self) } /// Rotates the vertex positions, normals, and tangents of the mesh in place by the given [`Quat`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_rotate_by`] - pub fn rotate_by(&mut self, rotation: Quat) { - self.try_rotate_by(rotation).expect(MESH_EXTRACTED_ERROR); - } - - /// Rotates the vertex positions, normals, and tangents of the mesh in place by the given [`Quat`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_rotate_by(&mut self, rotation: Quat) -> Result<(), MeshAccessError> { + pub fn rotate_by(&mut self, rotation: Quat) -> Result<(), MeshAccessError> { if let Some(VertexAttributeValues::Float32x3(positions)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? + self.attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? { // Apply rotation to vertex positions positions @@ -1902,7 +1999,7 @@ impl Mesh { } if let Some(VertexAttributeValues::Float32x3(normals)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_NORMAL)? + self.attribute_mut_option(Mesh::ATTRIBUTE_NORMAL)? { // Transform normals normals.iter_mut().for_each(|normal| { @@ -1911,7 +2008,7 @@ impl Mesh { } if let Some(VertexAttributeValues::Float32x4(tangents)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_TANGENT)? + self.attribute_mut_option(Mesh::ATTRIBUTE_TANGENT)? { // Transform tangents tangents.iter_mut().for_each(|tangent| { @@ -1928,38 +2025,15 @@ impl Mesh { /// Scales the vertex positions, normals, and tangents of the mesh by the given [`Vec3`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_scaled_by`] - pub fn scaled_by(mut self, scale: Vec3) -> Self { - self.scale_by(scale); - self - } - - /// Scales the vertex positions, normals, and tangents of the mesh by the given [`Vec3`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_scaled_by(mut self, scale: Vec3) -> Result { - self.try_scale_by(scale)?; + pub fn scaled_by(mut self, scale: Vec3) -> Result { + self.scale_by(scale)?; Ok(self) } /// Scales the vertex positions, normals, and tangents of the mesh in place by the given [`Vec3`]. /// /// `Aabb` of entities with modified mesh are not updated automatically. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_scale_by`] - pub fn scale_by(&mut self, scale: Vec3) { - self.try_scale_by(scale).expect(MESH_EXTRACTED_ERROR); - } - - /// Scales the vertex positions, normals, and tangents of the mesh in place by the given [`Vec3`]. - /// - /// `Aabb` of entities with modified mesh are not updated automatically. - pub fn try_scale_by(&mut self, scale: Vec3) -> Result<(), MeshAccessError> { + pub fn scale_by(&mut self, scale: Vec3) -> Result<(), MeshAccessError> { // Needed when transforming normals and tangents let scale_recip = 1. / scale; debug_assert!( @@ -1968,7 +2042,7 @@ impl Mesh { ); if let Some(VertexAttributeValues::Float32x3(positions)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? + self.attribute_mut_option(Mesh::ATTRIBUTE_POSITION)? { // Apply scale to vertex positions positions @@ -1982,7 +2056,7 @@ impl Mesh { } if let Some(VertexAttributeValues::Float32x3(normals)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_NORMAL)? + self.attribute_mut_option(Mesh::ATTRIBUTE_NORMAL)? { // Transform normals, taking into account non-uniform scaling normals.iter_mut().for_each(|normal| { @@ -1991,7 +2065,7 @@ impl Mesh { } if let Some(VertexAttributeValues::Float32x4(tangents)) = - self.try_attribute_mut_option(Mesh::ATTRIBUTE_TANGENT)? + self.attribute_mut_option(Mesh::ATTRIBUTE_TANGENT)? { // Transform tangents, taking into account non-uniform scaling tangents.iter_mut().for_each(|tangent| { @@ -2008,19 +2082,9 @@ impl Mesh { } /// Normalize joint weights so they sum to 1. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_normalize_joint_weights`] - pub fn normalize_joint_weights(&mut self) { - self.try_normalize_joint_weights() - .expect(MESH_EXTRACTED_ERROR); - } - - /// Normalize joint weights so they sum to 1. - pub fn try_normalize_joint_weights(&mut self) -> Result<(), MeshAccessError> { + pub fn normalize_joint_weights(&mut self) -> Result<(), MeshAccessError> { if let Some(VertexAttributeValues::Float32x4(joints)) = - self.try_attribute_mut_option(Self::ATTRIBUTE_JOINT_WEIGHT)? + self.attribute_mut_option(Self::ATTRIBUTE_JOINT_WEIGHT)? { for weights in joints.iter_mut() { // force negative weights to zero @@ -2052,13 +2116,13 @@ impl Mesh { /// [primitive topology]: PrimitiveTopology /// [triangles]: Triangle3d pub fn triangles(&self) -> Result + '_, MeshTrianglesError> { - let position_data = self.try_attribute(Mesh::ATTRIBUTE_POSITION)?; + let position_data = self.attribute(Mesh::ATTRIBUTE_POSITION)?; let Some(vertices) = position_data.as_float3() else { return Err(MeshTrianglesError::PositionsFormat); }; - let indices = self.try_indices()?; + let indices = self.indices()?; match self.primitive_topology { PrimitiveTopology::TriangleList => { @@ -2183,35 +2247,22 @@ impl Mesh { #[cfg(feature = "morph")] impl Mesh { /// Whether this mesh has morph targets. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_has_morph_targets`] - pub fn has_morph_targets(&self) -> bool { - self.try_has_morph_targets().expect(MESH_EXTRACTED_ERROR) - } - - /// Whether this mesh has morph targets. - pub fn try_has_morph_targets(&self) -> Result { + pub fn has_morph_targets(&self) -> Result { Ok(self.morph_targets.as_ref_option()?.is_some()) } - /// Set [morph targets] image for this mesh. This requires a "morph target image". See [`MorphTargetImage`](crate::morph::MorphTargetImage) for info. - /// - /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation + /// Retrieve the morph targets for this mesh, or None if there are no morph targets. /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_set_morph_targets`] - pub fn set_morph_targets(&mut self, morph_targets: Handle) { - self.try_set_morph_targets(morph_targets) - .expect(MESH_EXTRACTED_ERROR); + /// Returns an error if the mesh data has been extracted to `RenderWorld`or + /// if the morph targets do not exist. + pub fn morph_targets(&self) -> Result<&Handle, MeshAccessError> { + self.morph_targets.as_ref() } /// Set [morph targets] image for this mesh. This requires a "morph target image". See [`MorphTargetImage`](crate::morph::MorphTargetImage) for info. /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation - pub fn try_set_morph_targets( + pub fn set_morph_targets( &mut self, morph_targets: Handle, ) -> Result<(), MeshAccessError> { @@ -2219,41 +2270,6 @@ impl Mesh { Ok(()) } - /// Retrieve the morph targets for this mesh, or None if there are no morph targets. - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_morph_targets`] - pub fn morph_targets(&self) -> Option<&Handle> { - self.morph_targets - .as_ref_option() - .expect(MESH_EXTRACTED_ERROR) - } - - /// Retrieve the morph targets for this mesh, or None if there are no morph targets. - /// - /// Returns an error if the mesh data has been extracted to `RenderWorld`or - /// if the morph targets do not exist. - pub fn try_morph_targets(&self) -> Result<&Handle, MeshAccessError> { - self.morph_targets.as_ref() - } - - /// Consumes the mesh and returns a mesh with the given [morph targets]. - /// - /// This requires a "morph target image". See [`MorphTargetImage`](crate::morph::MorphTargetImage) for info. - /// - /// (Alternatively, you can use [`Mesh::set_morph_targets`] to mutate an existing mesh in-place) - /// - /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_with_morph_targets`] - #[must_use] - pub fn with_morph_targets(mut self, morph_targets: Handle) -> Self { - self.set_morph_targets(morph_targets); - self - } - /// Consumes the mesh and returns a mesh with the given [morph targets]. /// /// This requires a "morph target image". See [`MorphTargetImage`](crate::morph::MorphTargetImage) for info. @@ -2263,77 +2279,38 @@ impl Mesh { /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_with_morph_targets( + pub fn with_morph_targets( mut self, morph_targets: Handle, ) -> Result { - self.try_set_morph_targets(morph_targets)?; + self.set_morph_targets(morph_targets)?; Ok(self) } - /// Sets the names of each morph target. This should correspond to the order of the morph targets in `set_morph_targets`. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_set_morph_target_names`] - pub fn set_morph_target_names(&mut self, names: Vec) { - self.try_set_morph_target_names(names) - .expect(MESH_EXTRACTED_ERROR); - } - /// Sets the names of each morph target. This should correspond to the order of the morph targets in `set_morph_targets`. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_set_morph_target_names( - &mut self, - names: Vec, - ) -> Result<(), MeshAccessError> { + pub fn set_morph_target_names(&mut self, names: Vec) -> Result<(), MeshAccessError> { self.morph_target_names.replace(Some(names))?; Ok(()) } - /// Consumes the mesh and returns a mesh with morph target names. - /// Names should correspond to the order of the morph targets in `set_morph_targets`. - /// - /// (Alternatively, you can use [`Mesh::set_morph_target_names`] to mutate an existing mesh in-place) - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_set_morph_target_names`] - #[must_use] - pub fn with_morph_target_names(self, names: Vec) -> Self { - self.try_with_morph_target_names(names) - .expect(MESH_EXTRACTED_ERROR) - } - /// Consumes the mesh and returns a mesh with morph target names. /// Names should correspond to the order of the morph targets in `set_morph_targets`. /// /// (Alternatively, you can use [`Mesh::set_morph_target_names`] to mutate an existing mesh in-place) /// /// Returns an error if the mesh data has been extracted to `RenderWorld`. - pub fn try_with_morph_target_names( - mut self, - names: Vec, - ) -> Result { - self.try_set_morph_target_names(names)?; + pub fn with_morph_target_names(mut self, names: Vec) -> Result { + self.set_morph_target_names(names)?; Ok(self) } - /// Gets a list of all morph target names, if they exist. - /// - /// # Panics - /// Panics when the mesh data has already been extracted to `RenderWorld`. To handle - /// this as an error use [`Mesh::try_morph_target_names`] - pub fn morph_target_names(&self) -> Option<&[String]> { - self.try_morph_target_names().expect(MESH_EXTRACTED_ERROR) - } - /// Gets a list of all morph target names, if they exist. /// /// Returns an error if the mesh data has been extracted to `RenderWorld`or /// if the morph targets do not exist. - pub fn try_morph_target_names(&self) -> Result, MeshAccessError> { + pub fn morph_target_names(&self) -> Result, MeshAccessError> { Ok(self .morph_target_names .as_ref_option()? @@ -2360,7 +2337,7 @@ impl core::ops::Mul for Transform { type Output = Mesh; fn mul(self, rhs: Mesh) -> Self::Output { - rhs.transformed_by(self) + rhs.transformed_by(self).expect(MESH_EXTRACTED_ERROR) } } @@ -2517,9 +2494,9 @@ pub enum MeshMergeError { #[cfg(test)] mod tests { - use super::Mesh; #[cfg(feature = "serialize")] use super::SerializedMesh; + use super::{InfallibleMesh, Mesh}; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; use crate::PrimitiveTopology; use bevy_asset::RenderAssetUsages; @@ -2539,7 +2516,7 @@ mod tests { #[test] fn transform_mesh() { - let mesh = Mesh::new( + let mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -2591,7 +2568,7 @@ mod tests { #[test] fn point_list_mesh_invert_winding() { - let mesh = Mesh::new(PrimitiveTopology::PointList, RenderAssetUsages::default()) + let mesh = InfallibleMesh::new(PrimitiveTopology::PointList, RenderAssetUsages::default()) .with_inserted_indices(Indices::U32(vec![])); assert!(matches!( mesh.with_inverted_winding(), @@ -2601,7 +2578,7 @@ mod tests { #[test] fn line_list_mesh_invert_winding() { - let mesh = Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + let mesh = InfallibleMesh::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!( @@ -2612,7 +2589,7 @@ mod tests { #[test] fn line_list_mesh_invert_winding_fail() { - let mesh = Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + let mesh = InfallibleMesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) .with_inserted_indices(Indices::U32(vec![0, 1, 1])); assert!(matches!( mesh.with_inverted_winding(), @@ -2622,7 +2599,7 @@ mod tests { #[test] fn line_strip_mesh_invert_winding() { - let mesh = Mesh::new(PrimitiveTopology::LineStrip, RenderAssetUsages::default()) + let mesh = InfallibleMesh::new(PrimitiveTopology::LineStrip, RenderAssetUsages::default()) .with_inserted_indices(Indices::U32(vec![0, 1, 2, 3])); let mesh = mesh.with_inverted_winding().unwrap(); assert_eq!( @@ -2633,7 +2610,7 @@ mod tests { #[test] fn triangle_list_mesh_invert_winding() { - let mesh = Mesh::new( + let mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -2653,7 +2630,7 @@ mod tests { #[test] fn triangle_list_mesh_invert_winding_fail() { - let mesh = Mesh::new( + let mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -2666,7 +2643,7 @@ mod tests { #[test] fn triangle_strip_mesh_invert_winding() { - let mesh = Mesh::new( + let mesh = InfallibleMesh::new( PrimitiveTopology::TriangleStrip, RenderAssetUsages::default(), ) @@ -2680,7 +2657,7 @@ mod tests { #[test] fn compute_area_weighted_normals() { - let mut mesh = Mesh::new( + let mut mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); @@ -2715,7 +2692,7 @@ mod tests { #[test] fn compute_area_weighted_normals_proportionate() { - let mut mesh = Mesh::new( + let mut mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); @@ -2777,7 +2754,7 @@ mod tests { 4, 5, 1, 1, 0, 4, // top 3, 2, 6, 6, 7, 3, // bottom ]); - let mut mesh = Mesh::new( + let mut mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); @@ -2803,7 +2780,7 @@ mod tests { #[test] fn triangles_from_triangle_list() { - let mut mesh = Mesh::new( + let mut mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); @@ -2835,7 +2812,7 @@ mod tests { #[test] fn triangles_from_triangle_strip() { - let mut mesh = Mesh::new( + let mut mesh = InfallibleMesh::new( PrimitiveTopology::TriangleStrip, RenderAssetUsages::default(), ); @@ -2881,7 +2858,7 @@ mod tests { #[cfg(feature = "serialize")] #[test] fn serialize_deserialize_mesh() { - let mut mesh = Mesh::new( + let mut mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ); @@ -2892,6 +2869,8 @@ mod tests { ); mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3])); + let mesh = mesh.into_mesh(); + let serialized_mesh = SerializedMesh::from_mesh(mesh.clone()); let serialized_string = serde_json::to_string(&serialized_mesh).unwrap(); let serialized_mesh_from_string: SerializedMesh = diff --git a/crates/bevy_mesh/src/mikktspace.rs b/crates/bevy_mesh/src/mikktspace.rs index 36b24353c7c13..528b5d076aa00 100644 --- a/crates/bevy_mesh/src/mikktspace.rs +++ b/crates/bevy_mesh/src/mikktspace.rs @@ -84,7 +84,7 @@ pub(crate) fn generate_tangents_for_mesh( other => return Err(GenerateTangentsError::UnsupportedTopology(other)), }; - let positions = mesh.try_attribute_option(Mesh::ATTRIBUTE_POSITION)?.ok_or( + let positions = mesh.attribute_option(Mesh::ATTRIBUTE_POSITION)?.ok_or( GenerateTangentsError::MissingVertexAttribute(Mesh::ATTRIBUTE_POSITION.name), )?; let VertexAttributeValues::Float32x3(positions) = positions else { @@ -93,7 +93,7 @@ pub(crate) fn generate_tangents_for_mesh( VertexFormat::Float32x3, )); }; - let normals = mesh.try_attribute_option(Mesh::ATTRIBUTE_NORMAL)?.ok_or( + let normals = mesh.attribute_option(Mesh::ATTRIBUTE_NORMAL)?.ok_or( GenerateTangentsError::MissingVertexAttribute(Mesh::ATTRIBUTE_NORMAL.name), )?; let VertexAttributeValues::Float32x3(normals) = normals else { @@ -102,7 +102,7 @@ pub(crate) fn generate_tangents_for_mesh( VertexFormat::Float32x3, )); }; - let uvs = mesh.try_attribute_option(Mesh::ATTRIBUTE_UV_0)?.ok_or( + let uvs = mesh.attribute_option(Mesh::ATTRIBUTE_UV_0)?.ok_or( GenerateTangentsError::MissingVertexAttribute(Mesh::ATTRIBUTE_UV_0.name), )?; let VertexAttributeValues::Float32x2(uvs) = uvs else { @@ -115,7 +115,7 @@ pub(crate) fn generate_tangents_for_mesh( let len = positions.len(); let tangents = vec![[0., 0., 0., 0.]; len]; let mut mikktspace_mesh = MikktspaceGeometryHelper { - indices: mesh.try_indices_option()?, + indices: mesh.indices_option()?, positions, normals, uvs, diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs index 2a8bb4edf50cf..960bf8b1231c8 100644 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ b/crates/bevy_mesh/src/primitives/dim2.rs @@ -1,6 +1,7 @@ use core::f32::consts::FRAC_PI_2; use core::mem; +use crate::InfallibleMesh; use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment, VertexAttributeValues}; use bevy_asset::RenderAssetUsages; @@ -59,11 +60,11 @@ impl CircleMeshBuilder { } impl MeshBuilder for CircleMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { Ellipse::new(self.circle.radius, self.circle.radius) .mesh() .resolution(self.resolution) - .build() + .build_infallible() } } @@ -175,7 +176,7 @@ impl CircularSectorMeshBuilder { } impl MeshBuilder for CircularSectorMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let resolution = self.resolution as usize; let mut indices = Vec::with_capacity((resolution - 1) * 3); let mut positions = Vec::with_capacity(resolution + 1); @@ -210,7 +211,7 @@ impl MeshBuilder for CircularSectorMeshBuilder { indices.extend_from_slice(&[0, i, i + 1]); } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -313,7 +314,7 @@ impl CircularSegmentMeshBuilder { } impl MeshBuilder for CircularSegmentMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let resolution = self.resolution as usize; let mut indices = Vec::with_capacity((resolution - 1) * 3); let mut positions = Vec::with_capacity(resolution + 1); @@ -357,7 +358,7 @@ impl MeshBuilder for CircularSegmentMeshBuilder { indices.extend_from_slice(&[0, i, i + 1]); } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -427,7 +428,7 @@ impl Meshable for ConvexPolygon { } impl MeshBuilder for ConvexPolygonMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let len = self.vertices.len(); let mut indices = Vec::with_capacity((len - 2) * 3); let mut positions = Vec::with_capacity(len); @@ -438,7 +439,7 @@ impl MeshBuilder for ConvexPolygonMeshBuilder { for i in 2..len as u32 { indices.extend_from_slice(&[0, i - 1, i]); } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -512,12 +513,12 @@ impl Meshable for RegularPolygon { } impl MeshBuilder for RegularPolygonMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { // The ellipse mesh is just a regular polygon with two radii Ellipse::new(self.circumradius, self.circumradius) .mesh() .resolution(self.sides) - .build() + .build_infallible() } } @@ -576,7 +577,7 @@ impl EllipseMeshBuilder { } impl MeshBuilder for EllipseMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let resolution = self.resolution as usize; let mut indices = Vec::with_capacity((resolution - 2) * 3); let mut positions = Vec::with_capacity(resolution); @@ -602,7 +603,7 @@ impl MeshBuilder for EllipseMeshBuilder { indices.extend_from_slice(&[0, i, i + 1]); } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -655,11 +656,11 @@ impl Segment2dMeshBuilder { } impl MeshBuilder for Segment2dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let positions = self.segment.vertices.map(|v| v.extend(0.0)).to_vec(); let indices = Indices::U32(vec![0, 1]); - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + InfallibleMesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) .with_inserted_indices(indices) } @@ -688,7 +689,7 @@ pub struct Polyline2dMeshBuilder { } impl MeshBuilder for Polyline2dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let positions: Vec<_> = self .polyline .vertices @@ -702,7 +703,7 @@ impl MeshBuilder for Polyline2dMeshBuilder { .collect(), ); - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + InfallibleMesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) .with_inserted_indices(indices) .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) } @@ -764,7 +765,7 @@ impl AnnulusMeshBuilder { } impl MeshBuilder for AnnulusMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let inner_radius = self.annulus.inner_circle.radius; let outer_radius = self.annulus.outer_circle.radius; @@ -811,7 +812,7 @@ impl MeshBuilder for AnnulusMeshBuilder { indices.extend_from_slice(&[next_outer, next_inner, inner_vertex]); } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -896,7 +897,7 @@ impl RhombusMeshBuilder { } impl MeshBuilder for RhombusMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let [hhd, vhd] = [self.half_diagonals.x, self.half_diagonals.y]; let positions = vec![ [hhd, 0.0, 0.0], @@ -908,7 +909,7 @@ impl MeshBuilder for RhombusMeshBuilder { let uvs = vec![[1.0, 0.5], [0.5, 0.0], [0.0, 0.5], [0.5, 1.0]]; let indices = Indices::U32(vec![2, 0, 1, 2, 3, 0]); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -968,7 +969,7 @@ impl Meshable for Triangle2d { } impl MeshBuilder for Triangle2dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let vertices_3d = self.triangle.vertices.map(|v| v.extend(0.)); let positions: Vec<_> = vertices_3d.into(); @@ -988,7 +989,7 @@ impl MeshBuilder for Triangle2dMeshBuilder { Indices::U32(vec![2, 1, 0]) }; - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -1053,7 +1054,7 @@ impl RectangleMeshBuilder { } impl MeshBuilder for RectangleMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let [hw, hh] = [self.half_size.x, self.half_size.y]; let positions = vec![ [hw, hh, 0.0], @@ -1065,7 +1066,7 @@ impl MeshBuilder for RectangleMeshBuilder { let uvs = vec![[1.0, 0.0], [0.0, 0.0], [0.0, 1.0], [1.0, 1.0]]; let indices = Indices::U32(vec![0, 1, 2, 0, 2, 3]); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -1143,7 +1144,7 @@ impl Capsule2dMeshBuilder { } impl MeshBuilder for Capsule2dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { // The resolution is the number of vertices for one semicircle let resolution = self.resolution; let vertex_count = 2 * resolution; @@ -1207,7 +1208,7 @@ impl MeshBuilder for Capsule2dMeshBuilder { // Add indices for bottom right triangle of the part between the semicircles indices.extend_from_slice(&[resolution, vertex_count - 1, 0]); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -1290,7 +1291,7 @@ where } fn get_vertex_attributes(&self) -> Option { - fn get_positions(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { + fn get_positions(mesh: &mut InfallibleMesh) -> Option<&mut Vec<[f32; 3]>> { if let VertexAttributeValues::Float32x3(data) = mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION)? { @@ -1300,7 +1301,7 @@ where } } - fn get_uvs(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 2]>> { + fn get_uvs(mesh: &mut InfallibleMesh) -> Option<&mut Vec<[f32; 2]>> { if let VertexAttributeValues::Float32x2(data) = mesh.attribute_mut(Mesh::ATTRIBUTE_UV_0)? { @@ -1310,7 +1311,7 @@ where } } - fn get_normals(mesh: &mut Mesh) -> Option<&mut Vec<[f32; 3]>> { + fn get_normals(mesh: &mut InfallibleMesh) -> Option<&mut Vec<[f32; 3]>> { if let VertexAttributeValues::Float32x3(data) = mesh.attribute_mut(Mesh::ATTRIBUTE_NORMAL)? { @@ -1320,8 +1321,8 @@ where } } - let mut outer = self.outer_shape_builder.build(); - let mut inner = self.inner_shape_builder.build(); + let mut outer = self.outer_shape_builder.build_infallible(); + let mut inner = self.inner_shape_builder.build_infallible(); assert_eq!( outer.primitive_topology(), @@ -1371,7 +1372,7 @@ where /// It is assumed that the `primitive_topology` of the mesh returned by /// the underlying builder is [`PrimitiveTopology::TriangleList`] /// and that the mesh has [`Mesh::ATTRIBUTE_POSITION`], [`Mesh::ATTRIBUTE_NORMAL`] and [`Mesh::ATTRIBUTE_UV_0`] attributes. - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { if let Some(RingMeshBuilderVertexAttributes { outer_uvs, inner_uvs, @@ -1424,7 +1425,7 @@ where let mut positions = outer_positions; positions.extend_from_slice(&inner_positions); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -1569,7 +1570,7 @@ mod tests { #[test] fn test_regular_polygon() { - let mut mesh = Mesh::from(RegularPolygon::new(7.0, 4)); + let mut mesh = RegularPolygon::new(7.0, 4).mesh().build_infallible(); let Some(VertexAttributeValues::Float32x3(mut positions)) = mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) diff --git a/crates/bevy_mesh/src/primitives/dim3/capsule.rs b/crates/bevy_mesh/src/primitives/dim3/capsule.rs index f46ebce0d15a7..64f5ae86ee6f3 100644 --- a/crates/bevy_mesh/src/primitives/dim3/capsule.rs +++ b/crates/bevy_mesh/src/primitives/dim3/capsule.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, primitives::Capsule3d, Vec2, Vec3}; use bevy_reflect::prelude::*; @@ -94,7 +94,7 @@ impl Capsule3dMeshBuilder { } impl MeshBuilder for Capsule3dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { // code adapted from https://behreajj.medium.com/making-a-capsule-mesh-via-script-in-five-3d-environments-c2214abf02db let Capsule3dMeshBuilder { capsule, @@ -407,7 +407,7 @@ impl MeshBuilder for Capsule3dMeshBuilder { assert_eq!(vs.len(), vert_len); assert_eq!(tris.len(), fs_len as usize); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/cone.rs b/crates/bevy_mesh/src/primitives/dim3/cone.rs index d06a57f832ea5..b773f05a71e74 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cone.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cone.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, primitives::Cone, Vec3}; use bevy_reflect::prelude::*; @@ -69,7 +69,7 @@ impl ConeMeshBuilder { } impl MeshBuilder for ConeMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let half_height = self.cone.height / 2.0; // `resolution` vertices for the base, `resolution` vertices for the bottom of the lateral surface, @@ -160,7 +160,7 @@ impl MeshBuilder for ConeMeshBuilder { ConeAnchor::MidPoint => (), }; - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -213,7 +213,7 @@ mod tests { } .mesh() .resolution(4) - .build(); + .build_infallible(); let Some(VertexAttributeValues::Float32x3(mut positions)) = mesh.remove_attribute(Mesh::ATTRIBUTE_POSITION) diff --git a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs index 8c69378c01e64..a0ca1fe9519ec 100644 --- a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs +++ b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, primitives::ConicalFrustum, Vec3}; use bevy_reflect::prelude::*; @@ -61,7 +61,7 @@ impl ConicalFrustumMeshBuilder { } impl MeshBuilder for ConicalFrustumMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { debug_assert!(self.resolution > 2); debug_assert!(self.segments > 0); @@ -155,7 +155,7 @@ impl MeshBuilder for ConicalFrustumMeshBuilder { build_cap(true, radius_top); build_cap(false, radius_bottom); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs index 40a7cd45d4433..7edd0c3d71945 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{primitives::Cuboid, Vec3}; use bevy_reflect::prelude::*; @@ -20,7 +20,7 @@ impl Default for CuboidMeshBuilder { } impl MeshBuilder for CuboidMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let min = -self.half_size; let max = self.half_size; @@ -71,7 +71,7 @@ impl MeshBuilder for CuboidMeshBuilder { 20, 21, 22, 22, 23, 20, // bottom ]); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs index 7b1b45974ea62..dc8956243f5c5 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, primitives::Cylinder}; use bevy_reflect::prelude::*; @@ -94,7 +94,7 @@ impl CylinderMeshBuilder { } impl MeshBuilder for CylinderMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let resolution = self.resolution; let segments = self.segments; @@ -193,7 +193,7 @@ impl MeshBuilder for CylinderMeshBuilder { CylinderAnchor::MidPoint => (), }; - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/plane.rs b/crates/bevy_mesh/src/primitives/dim3/plane.rs index fd892469be6af..3b174f5bb48f8 100644 --- a/crates/bevy_mesh/src/primitives/dim3/plane.rs +++ b/crates/bevy_mesh/src/primitives/dim3/plane.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{primitives::Plane3d, Dir3, Quat, Vec2, Vec3}; use bevy_reflect::prelude::*; @@ -94,7 +94,7 @@ impl PlaneMeshBuilder { } impl MeshBuilder for PlaneMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let z_vertex_count = self.subdivisions + 2; let x_vertex_count = self.subdivisions + 2; let num_vertices = (z_vertex_count * x_vertex_count) as usize; @@ -131,7 +131,7 @@ impl MeshBuilder for PlaneMeshBuilder { } } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs index 4d13112579f09..f0e8ae42b0c54 100644 --- a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::primitives::Polyline3d; use bevy_reflect::prelude::*; @@ -11,7 +11,7 @@ pub struct Polyline3dMeshBuilder { } impl MeshBuilder for Polyline3dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let positions: Vec<_> = self.polyline.vertices.clone(); let indices = Indices::U32( @@ -20,7 +20,7 @@ impl MeshBuilder for Polyline3dMeshBuilder { .collect(), ); - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + InfallibleMesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) .with_inserted_indices(indices) .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) } diff --git a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs index d032285283afb..4326955cb2324 100644 --- a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::primitives::Segment3d; use bevy_reflect::prelude::*; @@ -11,11 +11,11 @@ pub struct Segment3dMeshBuilder { } impl MeshBuilder for Segment3dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let positions: Vec<_> = self.segment.vertices.into(); let indices = Indices::U32(vec![0, 1]); - Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + InfallibleMesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) .with_inserted_indices(indices) .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) } diff --git a/crates/bevy_mesh/src/primitives/dim3/sphere.rs b/crates/bevy_mesh/src/primitives/dim3/sphere.rs index 6ae8eec5ed8bf..946b850430262 100644 --- a/crates/bevy_mesh/src/primitives/dim3/sphere.rs +++ b/crates/bevy_mesh/src/primitives/dim3/sphere.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, primitives::Sphere}; use bevy_reflect::prelude::*; @@ -81,7 +81,7 @@ impl SphereMeshBuilder { /// and an [`IcosphereError`] is returned. /// /// A good default is `5` subdivisions. - pub fn ico(&self, subdivisions: u32) -> Result { + pub fn ico(&self, subdivisions: u32) -> Result { if subdivisions >= 80 { /* Number of triangles: @@ -151,7 +151,7 @@ impl SphereMeshBuilder { let indices = Indices::U32(indices); - Ok(Mesh::new( + Ok(InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -165,7 +165,7 @@ impl SphereMeshBuilder { /// longitudinal sectors and latitudinal stacks, aka horizontal and vertical resolution. /// /// A good default is `32` sectors and `18` stacks. - pub fn uv(&self, sectors: u32, stacks: u32) -> Mesh { + pub fn uv(&self, sectors: u32, stacks: u32) -> InfallibleMesh { // Largely inspired from http://www.songho.ca/opengl/gl_sphere.html let sectors_f32 = sectors as f32; @@ -220,7 +220,7 @@ impl SphereMeshBuilder { } } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -238,7 +238,7 @@ impl MeshBuilder for SphereMeshBuilder { /// /// Panics if the sphere is a [`SphereKind::Ico`] with a subdivision count /// that is greater than or equal to `80` because there will be too many vertices. - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { match self.kind { SphereKind::Ico { subdivisions } => self.ico(subdivisions).unwrap(), SphereKind::Uv { sectors, stacks } => self.uv(sectors, stacks), diff --git a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs index 529805d9a603f..432aa4337c4a5 100644 --- a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs +++ b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs @@ -1,5 +1,5 @@ use super::triangle3d; -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::primitives::{Tetrahedron, Triangle3d}; use bevy_reflect::prelude::*; @@ -12,7 +12,7 @@ pub struct TetrahedronMeshBuilder { } impl MeshBuilder for TetrahedronMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let mut faces: Vec<_> = self.tetrahedron.faces().into(); // If the tetrahedron has negative orientation, reverse all the triangles so that @@ -40,7 +40,7 @@ impl MeshBuilder for TetrahedronMeshBuilder { // There are four faces and none of them share vertices. let indices = Indices::U32(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/torus.rs b/crates/bevy_mesh/src/primitives/dim3/torus.rs index 6f370c13418ca..d2267774c764a 100644 --- a/crates/bevy_mesh/src/primitives/dim3/torus.rs +++ b/crates/bevy_mesh/src/primitives/dim3/torus.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{ops, primitives::Torus, Vec3}; use bevy_reflect::prelude::*; @@ -77,7 +77,7 @@ impl TorusMeshBuilder { } impl MeshBuilder for TorusMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { // code adapted from http://apparat-engine.blogspot.com/2013/04/procedural-meshes-torus.html let n_vertices = (self.major_resolution + 1) * (self.minor_resolution + 1); @@ -147,7 +147,7 @@ impl MeshBuilder for TorusMeshBuilder { } } - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs index e35f272ab9bb1..2337feb512ecd 100644 --- a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs @@ -1,4 +1,4 @@ -use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use crate::{Indices, InfallibleMesh, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; use bevy_math::{primitives::Triangle3d, Vec3}; use bevy_reflect::prelude::*; @@ -11,7 +11,7 @@ pub struct Triangle3dMeshBuilder { } impl MeshBuilder for Triangle3dMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let positions: Vec<_> = self.triangle.vertices.into(); let uvs: Vec<_> = uv_coords(&self.triangle).into(); @@ -21,7 +21,7 @@ impl MeshBuilder for Triangle3dMeshBuilder { let indices = Indices::U32(vec![0, 1, 2]); - Mesh::new( + InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/crates/bevy_mesh/src/primitives/extrusion.rs b/crates/bevy_mesh/src/primitives/extrusion.rs index 091735dc8349c..2670805080904 100644 --- a/crates/bevy_mesh/src/primitives/extrusion.rs +++ b/crates/bevy_mesh/src/primitives/extrusion.rs @@ -4,7 +4,7 @@ use bevy_math::{ }; use super::{MeshBuilder, Meshable}; -use crate::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; +use crate::{Indices, InfallibleMesh, Mesh, PrimitiveTopology, VertexAttributeValues}; /// A type representing a segment of the perimeter of an extrudable mesh. pub enum PerimeterSegment { @@ -177,12 +177,12 @@ where P: Primitive2d + Meshable, P::Output: Extrudable, { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { // Create and move the base mesh to the front - let mut front_face = - self.base_builder - .build() - .translated_by(Vec3::new(0., 0., self.half_depth)); + let mut front_face = self + .base_builder + .build_infallible() + .translated_by(Vec3::new(0., 0., self.half_depth)); // Move the uvs of the front face to be between (0., 0.) and (0.5, 0.5) if let Some(VertexAttributeValues::Float32x2(uvs)) = @@ -413,7 +413,7 @@ where } } - Mesh::new(PrimitiveTopology::TriangleList, front_face.asset_usage) + InfallibleMesh::new(PrimitiveTopology::TriangleList, front_face.asset_usage) .with_inserted_indices(Indices::U32(indices)) .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) diff --git a/crates/bevy_mesh/src/primitives/mod.rs b/crates/bevy_mesh/src/primitives/mod.rs index 02aa0f1caf41e..64f0ab16a1242 100644 --- a/crates/bevy_mesh/src/primitives/mod.rs +++ b/crates/bevy_mesh/src/primitives/mod.rs @@ -28,6 +28,8 @@ pub use dim3::*; mod extrusion; pub use extrusion::*; +use crate::InfallibleMesh; + use super::Mesh; /// A trait for shapes that can be turned into a [`Mesh`]. @@ -41,8 +43,13 @@ pub trait Meshable { /// A trait used to build [`Mesh`]es from a configuration pub trait MeshBuilder { + /// Builds an [`InfallibleMesh`] based on the configuration in `self`. + fn build_infallible(&self) -> InfallibleMesh; + /// Builds a [`Mesh`] based on the configuration in `self`. - fn build(&self) -> Mesh; + fn build(&self) -> Mesh { + self.build_infallible().into() + } } impl From for Mesh { @@ -50,3 +57,9 @@ impl From for Mesh { builder.build() } } + +impl From for InfallibleMesh { + fn from(builder: T) -> Self { + builder.build_infallible() + } +} diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index fa38e084dd7c7..d51263eb621f0 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -32,7 +32,7 @@ impl Plugin for ForwardDecalPlugin { let mesh = app.world_mut().resource_mut::>().add( Rectangle::from_size(Vec2::ONE) .mesh() - .build() + .build_infallible() .rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) .with_generated_tangents() .unwrap(), diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 23de97909404f..ab4632500b341 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -7,7 +7,7 @@ use bevy_math::{ ops::log2, IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles, }; -use bevy_mesh::{Indices, Mesh}; +use bevy_mesh::{Indices, Mesh, MeshAccessError}; use bevy_platform::collections::HashMap; use bevy_render::render_resource::PrimitiveTopology; use bevy_tasks::{AsyncComputeTaskPool, ParallelSlice}; @@ -251,19 +251,19 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology); } - if mesh.attributes().map(|(attribute, _)| attribute.id).ne([ + if mesh.attributes()?.map(|(attribute, _)| attribute.id).ne([ Mesh::ATTRIBUTE_POSITION.id, Mesh::ATTRIBUTE_NORMAL.id, Mesh::ATTRIBUTE_UV_0.id, ]) { return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes( - mesh.attributes() + mesh.attributes()? .map(|(attribute, _)| format!("{attribute:?}")) .collect(), )); } - match mesh.indices() { + match mesh.indices_option()? { Some(Indices::U32(indices)) => Ok(Cow::Borrowed(indices.as_slice())), Some(Indices::U16(indices)) => Ok(indices.iter().map(|i| *i as u32).collect()), _ => Err(MeshToMeshletMeshConversionError::MeshMissingIndices), @@ -1088,4 +1088,6 @@ pub enum MeshToMeshletMeshConversionError { WrongMeshVertexAttributes(Vec), #[error("Mesh has no indices")] MeshMissingIndices, + #[error("{0}")] + MeshAccessError(#[from] MeshAccessError), } diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 24c6effab046c..8b13ef8994432 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -35,6 +35,8 @@ pub struct RayTriangleHit { pub barycentric_coords: (f32, f32), } +const MESH_EXTRACTED_ERROR: &str = "Cannot pick on extracted mesh, the mesh `asset_usage` must include `RenderAssetUsages::MAIN_WORLD`"; + /// Casts a ray on a mesh, and returns the intersection. pub(super) fn ray_intersection_over_mesh( mesh: &Mesh, @@ -46,21 +48,26 @@ pub(super) fn ray_intersection_over_mesh( return None; // ray_mesh_intersection assumes vertices are laid out in a triangle list } // Vertex positions are required - let positions = mesh.attribute(Mesh::ATTRIBUTE_POSITION)?.as_float3()?; + let positions = mesh + .attribute_option(Mesh::ATTRIBUTE_POSITION) + .expect(MESH_EXTRACTED_ERROR)? + .as_float3()?; // Normals are optional let normals = mesh - .attribute(Mesh::ATTRIBUTE_NORMAL) + .attribute_option(Mesh::ATTRIBUTE_NORMAL) + .expect("") .and_then(|normal_values| normal_values.as_float3()); let uvs = mesh - .attribute(Mesh::ATTRIBUTE_UV_0) + .attribute_option(Mesh::ATTRIBUTE_UV_0) + .expect(MESH_EXTRACTED_ERROR) .and_then(|uvs| match uvs { VertexAttributeValues::Float32x2(uvs) => Some(uvs.as_slice()), _ => None, }); - match mesh.indices() { + match mesh.indices_option().expect(MESH_EXTRACTED_ERROR) { Some(Indices::U16(indices)) => { ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull) } diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index 654114e758f22..73100f1ec7cd9 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -1029,7 +1029,10 @@ impl ElementLayout { /// Creates the appropriate [`ElementLayout`] for the given mesh's index /// data. fn index(mesh: &Mesh) -> Option { - let size = match mesh.indices()? { + let size = match mesh + .indices_option() + .expect("mesh data should always be available in render world")? + { Indices::U16(_) => 2, Indices::U32(_) => 4, }; diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index fb849cd51b3aa..33bc2a61a9bb8 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -144,7 +144,10 @@ impl RenderAsset for RenderMesh { fn byte_len(mesh: &Self::SourceAsset) -> Option { let mut vertex_size = 0; - for attribute_data in mesh.attributes() { + for attribute_data in mesh + .attributes() + .expect("mesh data should always be available in render world") + { let vertex_format = attribute_data.0.format; vertex_size += vertex_format.size() as usize; } @@ -163,21 +166,27 @@ impl RenderAsset for RenderMesh { ) -> Result> { #[cfg(feature = "morph")] let morph_targets = match mesh.morph_targets() { - Some(mt) => { + Ok(mt) => { let Some(target_image) = _images.get(mt) else { return Err(PrepareAssetError::RetryNextUpdate(mesh)); }; Some(target_image.texture_view.clone()) } - None => None, + Err(MeshAccessError::NotFound) => None, + Err(MeshAccessError::ExtractedToRenderWorld) => { + panic!("mesh data should always be available in render world") + } }; let buffer_info = match mesh.indices() { - Some(indices) => RenderMeshBufferInfo::Indexed { + Ok(indices) => RenderMeshBufferInfo::Indexed { count: indices.len() as u32, index_format: indices.into(), }, - None => RenderMeshBufferInfo::NonIndexed, + Err(MeshAccessError::NotFound) => RenderMeshBufferInfo::NonIndexed, + Err(MeshAccessError::ExtractedToRenderWorld) => { + panic!("mesh data should always be available in render world") + } }; let mesh_vertex_buffer_layout = @@ -185,7 +194,7 @@ impl RenderAsset for RenderMesh { let key_bits = BaseMeshPipelineKey::from_primitive_topology(mesh.primitive_topology()); #[cfg(feature = "morph")] - let key_bits = if mesh.morph_targets().is_some() { + let key_bits = if mesh.morph_targets().is_ok() { key_bits | BaseMeshPipelineKey::MORPH_TARGETS } else { key_bits diff --git a/crates/bevy_solari/src/scene/blas.rs b/crates/bevy_solari/src/scene/blas.rs index 58d6b7fd3abbb..a26078a91025a 100644 --- a/crates/bevy_solari/src/scene/blas.rs +++ b/crates/bevy_solari/src/scene/blas.rs @@ -172,12 +172,15 @@ fn allocate_blas( fn is_mesh_raytracing_compatible(mesh: &Mesh) -> bool { let triangle_list = mesh.primitive_topology() == PrimitiveTopology::TriangleList; - let vertex_attributes = mesh.attributes().map(|(attribute, _)| attribute.id).eq([ + let Ok(attributes) = mesh.attributes() else { + return false; + }; + let vertex_attributes = attributes.map(|(attribute, _)| attribute.id).eq([ Mesh::ATTRIBUTE_POSITION.id, Mesh::ATTRIBUTE_NORMAL.id, Mesh::ATTRIBUTE_UV_0.id, Mesh::ATTRIBUTE_TANGENT.id, ]); - let indexed_32 = matches!(mesh.indices(), Some(Indices::U32(..))); + let indexed_32 = matches!(mesh.indices(), Ok(Indices::U32(..))); mesh.enable_raytracing && triangle_list && vertex_attributes && indexed_32 } diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 79913288767a8..5b45daaca2cb6 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -10,7 +10,7 @@ use bevy::{ color::palettes::basic::YELLOW, core_pipeline::core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT}, math::{ops, FloatOrd}, - mesh::{Indices, MeshVertexAttribute, VertexBufferLayout}, + mesh::{Indices, InfallibleMesh, MeshVertexAttribute, VertexBufferLayout}, prelude::*, render::{ mesh::RenderMesh, @@ -58,7 +58,7 @@ fn star( // specified. We set `RenderAssetUsages::RENDER_WORLD`, meaning this mesh // will not be accessible in future frames from the `meshes` resource, in // order to save on memory once it has been uploaded to the GPU. - let mut star = Mesh::new( + let mut star = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::RENDER_WORLD, ); diff --git a/examples/2d/mesh2d_vertex_color_texture.rs b/examples/2d/mesh2d_vertex_color_texture.rs index bd6e8199ffb6d..fda92be3c97c3 100644 --- a/examples/2d/mesh2d_vertex_color_texture.rs +++ b/examples/2d/mesh2d_vertex_color_texture.rs @@ -1,7 +1,7 @@ //! Shows how to render a polygonal [`Mesh`], generated from a [`Rectangle`] primitive, in a 2D scene. //! Adds a texture and colored vertices, giving per-vertex tinting. -use bevy::prelude::*; +use bevy::{mesh::InfallibleMesh, prelude::*}; fn main() { App::new() @@ -19,7 +19,7 @@ fn setup( // Load the Bevy logo as a texture let texture_handle = asset_server.load("branding/banner.png"); // Build a default quad mesh - let mut mesh = Mesh::from(Rectangle::default()); + let mut mesh = InfallibleMesh::from(Rectangle::default().mesh()); // Build vertex colors for the quad. One entry per vertex (the corners of the quad) let vertex_colors: Vec<[f32; 4]> = vec![ LinearRgba::RED.to_f32_array(), diff --git a/examples/3d/generate_custom_mesh.rs b/examples/3d/generate_custom_mesh.rs index 7b4219effbd61..de0452caf7ded 100644 --- a/examples/3d/generate_custom_mesh.rs +++ b/examples/3d/generate_custom_mesh.rs @@ -4,7 +4,7 @@ use bevy::{ asset::RenderAssetUsages, - mesh::{Indices, VertexAttributeValues}, + mesh::{Indices, InfallibleMesh, VertexAttributeValues}, prelude::*, render::render_resource::PrimitiveTopology, }; @@ -103,9 +103,9 @@ fn input_handler( } #[rustfmt::skip] -fn create_cube_mesh() -> Mesh { +fn create_cube_mesh() -> InfallibleMesh { // Keep the mesh data accessible in future frames to be able to mutate it in toggle_texture. - Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD) + InfallibleMesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD) .with_inserted_attribute( Mesh::ATTRIBUTE_POSITION, // Each array is an [x, y, z] coordinate in local space. diff --git a/examples/3d/lines.rs b/examples/3d/lines.rs index c29e9fb45c0a8..4c2e3fdf29a49 100644 --- a/examples/3d/lines.rs +++ b/examples/3d/lines.rs @@ -2,7 +2,7 @@ use bevy::{ asset::RenderAssetUsages, - mesh::{MeshVertexBufferLayoutRef, PrimitiveTopology}, + mesh::{InfallibleMesh, MeshVertexBufferLayoutRef, PrimitiveTopology}, pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, reflect::TypePath, @@ -96,7 +96,7 @@ impl From for Mesh { fn from(line: LineList) -> Self { let vertices: Vec<_> = line.lines.into_iter().flat_map(|(a, b)| [a, b]).collect(); - Mesh::new( + InfallibleMesh::new( // This tells wgpu that the positions are list of lines // where every pair is a start and end point PrimitiveTopology::LineList, @@ -104,6 +104,7 @@ impl From for Mesh { ) // Add the vertices positions as an attribute .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, vertices) + .into() } } @@ -115,7 +116,7 @@ struct LineStrip { impl From for Mesh { fn from(line: LineStrip) -> Self { - Mesh::new( + InfallibleMesh::new( // This tells wgpu that the positions are a list of points // where a line will be drawn between each consecutive point PrimitiveTopology::LineStrip, @@ -123,5 +124,6 @@ impl From for Mesh { ) // Add the point positions as an attribute .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, line.points) + .into() } } diff --git a/examples/3d/motion_blur.rs b/examples/3d/motion_blur.rs index ef9349ee7b19d..50b9e9b2faf2c 100644 --- a/examples/3d/motion_blur.rs +++ b/examples/3d/motion_blur.rs @@ -4,6 +4,7 @@ use bevy::{ image::{ImageAddressMode, ImageFilterMode, ImageSampler, ImageSamplerDescriptor}, math::ops, + mesh::InfallibleMesh, post_process::motion_blur::MotionBlur, prelude::*, }; @@ -82,7 +83,7 @@ fn setup_scene( Transform::default().with_scale(Vec3::splat(-4000.0)), )); // Ground - let mut plane: Mesh = Plane3d::default().into(); + let mut plane: InfallibleMesh = Plane3d::default().mesh().into(); let uv_size = 4000.0; let uvs = vec![[uv_size, 0.0], [0.0, 0.0], [0.0, uv_size], [uv_size; 2]]; plane.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs index 6449b40d2bb66..e398332b9d8ca 100644 --- a/examples/3d/solari.rs +++ b/examples/3d/solari.rs @@ -179,19 +179,21 @@ fn add_raytracing_meshes_on_scene_load( // Ensure meshes are Solari compatible let mesh = meshes.get_mut(mesh_handle).unwrap(); - if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0) { + if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0).unwrap() { let vertex_count = mesh.count_vertices(); - mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]) + .unwrap(); mesh.insert_attribute( Mesh::ATTRIBUTE_TANGENT, vec![[0.0, 0.0, 0.0, 0.0]; vertex_count], - ); + ) + .unwrap(); } - if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT) { + if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT).unwrap() { mesh.generate_tangents().unwrap(); } - if mesh.contains_attribute(Mesh::ATTRIBUTE_UV_1) { - mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1); + if mesh.contains_attribute(Mesh::ATTRIBUTE_UV_1).unwrap() { + let _ = mesh.remove_attribute(Mesh::ATTRIBUTE_UV_1); } // Prevent rasterization if using pathtracer diff --git a/examples/3d/vertex_colors.rs b/examples/3d/vertex_colors.rs index 3a5c31f94a7bf..c16e0319fbee2 100644 --- a/examples/3d/vertex_colors.rs +++ b/examples/3d/vertex_colors.rs @@ -1,6 +1,9 @@ //! Illustrates the use of vertex colors. -use bevy::{mesh::VertexAttributeValues, prelude::*}; +use bevy::{ + mesh::{InfallibleMesh, VertexAttributeValues}, + prelude::*, +}; fn main() { App::new() @@ -22,7 +25,7 @@ fn setup( )); // cube // Assign vertex colors based on vertex positions - let mut colorful_cube = Mesh::from(Cuboid::default()); + let mut colorful_cube = InfallibleMesh::from(Cuboid::default().mesh()); if let Some(VertexAttributeValues::Float32x3(positions)) = colorful_cube.attribute(Mesh::ATTRIBUTE_POSITION) { diff --git a/examples/animation/custom_skinned_mesh.rs b/examples/animation/custom_skinned_mesh.rs index e45d21bbc2282..62ca5a7dfadba 100644 --- a/examples/animation/custom_skinned_mesh.rs +++ b/examples/animation/custom_skinned_mesh.rs @@ -8,7 +8,7 @@ use bevy::{ math::ops, mesh::{ skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, PrimitiveTopology, VertexAttributeValues, + Indices, InfallibleMesh, PrimitiveTopology, VertexAttributeValues, }, prelude::*, }; @@ -54,7 +54,7 @@ fn setup( ]); // Create a mesh - let mesh = Mesh::new( + let mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::RENDER_WORLD, ) diff --git a/examples/animation/morph_targets.rs b/examples/animation/morph_targets.rs index ffb468f26592c..675d8605434b2 100644 --- a/examples/animation/morph_targets.rs +++ b/examples/animation/morph_targets.rs @@ -86,7 +86,7 @@ fn name_morphs( if let AssetEvent::::Added { id } = event && let Some(path) = asset_server.get_path(*id) && let Some(mesh) = meshes.get(*id) - && let Some(names) = mesh.morph_target_names() + && let Some(names) = mesh.morph_target_names().unwrap() { info!("Morph target names for {path:?}:"); diff --git a/examples/asset/alter_mesh.rs b/examples/asset/alter_mesh.rs index d62178e10cbc5..92eb73572597f 100644 --- a/examples/asset/alter_mesh.rs +++ b/examples/asset/alter_mesh.rs @@ -188,7 +188,7 @@ fn alter_mesh( // `ATTRIBUTE_POSITION` is a constant indicating that we want to know where the vertex is // located in space (as opposed to which way its normal is facing, vertex color, or other // details). - if let Some(VertexAttributeValues::Float32x3(positions)) = + if let Ok(VertexAttributeValues::Float32x3(positions)) = mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) { // Check a Local value (which only this system can make use of) to determine if we're diff --git a/examples/gltf/query_gltf_primitives.rs b/examples/gltf/query_gltf_primitives.rs index 892d087e2f24a..7e87811eff414 100644 --- a/examples/gltf/query_gltf_primitives.rs +++ b/examples/gltf/query_gltf_primitives.rs @@ -35,7 +35,7 @@ fn find_top_material_and_mesh( } if let Some(mesh) = meshes.get_mut(mesh_handle) - && let Some(VertexAttributeValues::Float32x3(positions)) = + && let Ok(VertexAttributeValues::Float32x3(positions)) = mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) { for position in positions { diff --git a/examples/math/custom_primitives.rs b/examples/math/custom_primitives.rs index ba2976d7a8472..a24f033701000 100644 --- a/examples/math/custom_primitives.rs +++ b/examples/math/custom_primitives.rs @@ -17,7 +17,7 @@ use bevy::{ }, Isometry2d, }, - mesh::{Extrudable, ExtrusionBuilder, PerimeterSegment}, + mesh::{Extrudable, ExtrusionBuilder, InfallibleMesh, PerimeterSegment}, prelude::*, }; @@ -510,7 +510,7 @@ impl HeartBuilder for ExtrusionBuilder { impl MeshBuilder for HeartMeshBuilder { // This is where you should build the actual mesh. - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { let radius = self.heart.radius; // The curved parts of each wing (half) of the heart have an angle of `PI * 1.25` or 225° let wing_angle = PI * 1.25; @@ -553,7 +553,7 @@ impl MeshBuilder for HeartMeshBuilder { } // Here, the actual `Mesh` is created. We set the indices, vertices, normals and UVs created above and specify the topology of the mesh. - Mesh::new( + InfallibleMesh::new( bevy::mesh::PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/examples/math/sampling_primitives.rs b/examples/math/sampling_primitives.rs index b5b815f416064..75cbc77118d34 100644 --- a/examples/math/sampling_primitives.rs +++ b/examples/math/sampling_primitives.rs @@ -6,6 +6,7 @@ use bevy::{ core_pipeline::tonemapping::Tonemapping, input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButtonInput}, math::prelude::*, + mesh::InfallibleMesh, post_process::bloom::Bloom, prelude::*, }; @@ -206,7 +207,7 @@ impl Meshable for Shape { } impl MeshBuilder for ShapeMeshBuilder { - fn build(&self) -> Mesh { + fn build_infallible(&self) -> InfallibleMesh { match self.shape { Shape::Cuboid => CUBOID.mesh().into(), Shape::Sphere => SPHERE.mesh().into(), diff --git a/examples/shader_advanced/custom_vertex_attribute.rs b/examples/shader_advanced/custom_vertex_attribute.rs index 1741b08e39fea..95a645e389241 100644 --- a/examples/shader_advanced/custom_vertex_attribute.rs +++ b/examples/shader_advanced/custom_vertex_attribute.rs @@ -1,7 +1,7 @@ //! A shader that reads a mesh's custom vertex attribute. use bevy::{ - mesh::{MeshVertexAttribute, MeshVertexBufferLayoutRef}, + mesh::{InfallibleMesh, MeshVertexAttribute, MeshVertexBufferLayoutRef}, pbr::{MaterialPipeline, MaterialPipelineKey}, prelude::*, reflect::TypePath, @@ -32,7 +32,7 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, ) { - let mesh = Mesh::from(Cuboid::default()) + let mesh = InfallibleMesh::from(Cuboid::default().mesh()) // Sets the custom attribute .with_inserted_attribute( ATTRIBUTE_BLEND_COLOR, diff --git a/examples/shader_advanced/specialized_mesh_pipeline.rs b/examples/shader_advanced/specialized_mesh_pipeline.rs index ee5abdb54d647..35b79d1b3d80e 100644 --- a/examples/shader_advanced/specialized_mesh_pipeline.rs +++ b/examples/shader_advanced/specialized_mesh_pipeline.rs @@ -12,7 +12,7 @@ use bevy::{ core_pipeline::core_3d::{Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey, CORE_3D_DEPTH_FORMAT}, ecs::change_detection::Tick, math::{vec3, vec4}, - mesh::{Indices, MeshVertexBufferLayoutRef, PrimitiveTopology}, + mesh::{Indices, InfallibleMesh, MeshVertexBufferLayoutRef, PrimitiveTopology}, pbr::{ DrawMesh, MeshPipeline, MeshPipelineKey, MeshPipelineViewLayoutKey, RenderMeshInstances, SetMeshBindGroup, SetMeshViewBindGroup, SetMeshViewEmptyBindGroup, @@ -53,7 +53,7 @@ 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( + let mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) diff --git a/examples/tools/scene_viewer/morph_viewer_plugin.rs b/examples/tools/scene_viewer/morph_viewer_plugin.rs index 404c07d479e32..cc70943dd3f1c 100644 --- a/examples/tools/scene_viewer/morph_viewer_plugin.rs +++ b/examples/tools/scene_viewer/morph_viewer_plugin.rs @@ -260,7 +260,7 @@ fn detect_morphs( let target_names = weights .first_mesh() .and_then(|h| meshes.get(h)) - .and_then(|m| m.morph_target_names()); + .and_then(|m| m.morph_target_names().unwrap_or_default()); let targets = Target::new(name, weights.weights(), target_names, entity); detected.extend(targets); } diff --git a/tests/3d/test_invalid_skinned_mesh.rs b/tests/3d/test_invalid_skinned_mesh.rs index f371a06b7130e..a2ba66cda7b6e 100644 --- a/tests/3d/test_invalid_skinned_mesh.rs +++ b/tests/3d/test_invalid_skinned_mesh.rs @@ -6,7 +6,7 @@ use bevy::{ math::ops, mesh::{ skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, PrimitiveTopology, VertexAttributeValues, + Indices, InfallibleMesh, PrimitiveTopology, VertexAttributeValues, }, post_process::motion_blur::MotionBlur, prelude::*, @@ -96,7 +96,7 @@ fn setup_meshes( mut inverse_bindposes_assets: ResMut>, ) { // Create a mesh with two rectangles. - let unskinned_mesh = Mesh::new( + let unskinned_mesh = InfallibleMesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) From fe6bf7f48216013c009324ebdc529a20f8d5fa2d Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:28:21 +0000 Subject: [PATCH 130/133] fix doctest --- crates/bevy_mesh/src/mesh.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index a1ce73ddeb943..65930d470f27b 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -750,11 +750,11 @@ impl core::ops::Deref for InfallibleMesh { /// `StandardMaterial` or `ColorMaterial`: /// /// ``` -/// # use bevy_mesh::{Mesh, Indices, PrimitiveTopology}; +/// # use bevy_mesh::{Mesh, InfallibleMesh, Indices, PrimitiveTopology}; /// # use bevy_asset::RenderAssetUsages; /// fn create_simple_parallelogram() -> Mesh { /// // Create a new mesh using a triangle list topology, where each set of 3 vertices composes a triangle. -/// Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default()) +/// InfallibleMesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default()) /// // Add 4 vertices, each with its own position attribute (coordinate in /// // 3D space), for each of the corners of the parallelogram. /// .with_inserted_attribute( @@ -779,6 +779,7 @@ impl core::ops::Deref for InfallibleMesh { /// // Second triangle /// 1, 3, 2 /// ])) +/// .into() /// } /// ``` /// From 8c6e6bb857f98e341348324e3a0866eafbff77f1 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Sun, 30 Nov 2025 21:28:47 +0000 Subject: [PATCH 131/133] clean up From for Mesh --- crates/bevy_math/src/primitives/dim2.rs | 2 +- crates/bevy_mesh/src/primitives/dim2.rs | 146 +++--------------- .../bevy_mesh/src/primitives/dim3/capsule.rs | 10 +- crates/bevy_mesh/src/primitives/dim3/cone.rs | 10 +- .../src/primitives/dim3/conical_frustum.rs | 10 +- .../bevy_mesh/src/primitives/dim3/cuboid.rs | 8 +- .../bevy_mesh/src/primitives/dim3/cylinder.rs | 10 +- crates/bevy_mesh/src/primitives/dim3/plane.rs | 10 +- .../src/primitives/dim3/polyline3d.rs | 12 +- .../src/primitives/dim3/segment3d.rs | 10 +- .../bevy_mesh/src/primitives/dim3/sphere.rs | 10 +- .../src/primitives/dim3/tetrahedron.rs | 10 +- crates/bevy_mesh/src/primitives/dim3/torus.rs | 10 +- .../src/primitives/dim3/triangle3d.rs | 10 +- crates/bevy_mesh/src/primitives/extrusion.rs | 14 +- crates/bevy_mesh/src/primitives/mod.rs | 22 ++- examples/math/custom_primitives.rs | 4 +- examples/math/sampling_primitives.rs | 4 +- 18 files changed, 70 insertions(+), 242 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 572ff1473ce97..e0cc8f86da7f6 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1956,7 +1956,7 @@ impl From for Polygon { )] pub struct ConvexPolygon { /// The vertices of the [`ConvexPolygon`]. - vertices: Vec, + pub vertices: Vec, } #[cfg(feature = "alloc")] diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs index 960bf8b1231c8..cba9058820b0e 100644 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ b/crates/bevy_mesh/src/primitives/dim2.rs @@ -81,20 +81,14 @@ impl Extrudable for CircleMeshBuilder { impl Meshable for Circle { type Output = CircleMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { CircleMeshBuilder { - circle: *self, + circle: self, ..Default::default() } } } -impl From for Mesh { - fn from(circle: Circle) -> Self { - circle.mesh().build() - } -} - /// Specifies how to generate UV-mappings for the [`CircularSector`] and [`CircularSegment`] shapes. /// /// Currently the only variant is `Mask`, which is good for showing a portion of a texture that includes @@ -243,23 +237,14 @@ impl Extrudable for CircularSectorMeshBuilder { impl Meshable for CircularSector { type Output = CircularSectorMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { CircularSectorMeshBuilder { - sector: *self, + sector: self, ..Default::default() } } } -impl From for Mesh { - /// Converts this sector into a [`Mesh`] using a default [`CircularSectorMeshBuilder`]. - /// - /// See the documentation of [`CircularSectorMeshBuilder`] for more details. - fn from(sector: CircularSector) -> Self { - sector.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`CircularSegment`] shape. /// /// The resulting mesh will have a UV-map such that the center of the circle is @@ -390,23 +375,14 @@ impl Extrudable for CircularSegmentMeshBuilder { impl Meshable for CircularSegment { type Output = CircularSegmentMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { CircularSegmentMeshBuilder { - segment: *self, + segment: self, ..Default::default() } } } -impl From for Mesh { - /// Converts this sector into a [`Mesh`] using a default [`CircularSegmentMeshBuilder`]. - /// - /// See the documentation of [`CircularSegmentMeshBuilder`] for more details. - fn from(segment: CircularSegment) -> Self { - segment.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`ConvexPolygon`] shape. /// /// You must verify that the `vertices` are not concave when constructing this type. You can @@ -420,9 +396,9 @@ pub struct ConvexPolygonMeshBuilder { impl Meshable for ConvexPolygon { type Output = ConvexPolygonMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { Self::Output { - vertices: self.vertices().to_vec(), + vertices: self.vertices, } } } @@ -456,12 +432,6 @@ impl Extrudable for ConvexPolygonMeshBuilder { } } -impl From for Mesh { - fn from(polygon: ConvexPolygon) -> Self { - polygon.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`RegularPolygon`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -504,7 +474,7 @@ impl RegularPolygonMeshBuilder { impl Meshable for RegularPolygon { type Output = RegularPolygonMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { Self::Output { circumradius: self.circumcircle.radius, sides: self.sides, @@ -530,12 +500,6 @@ impl Extrudable for RegularPolygonMeshBuilder { } } -impl From for Mesh { - fn from(polygon: RegularPolygon) -> Self { - polygon.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with an [`Ellipse`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -627,20 +591,14 @@ impl Extrudable for EllipseMeshBuilder { impl Meshable for Ellipse { type Output = EllipseMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { EllipseMeshBuilder { - ellipse: *self, + ellipse: self, ..Default::default() } } } -impl From for Mesh { - fn from(ellipse: Ellipse) -> Self { - ellipse.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`Segment2d`]. pub struct Segment2dMeshBuilder { /// The [`Segment2d`] shape. @@ -669,15 +627,8 @@ impl MeshBuilder for Segment2dMeshBuilder { impl Meshable for Segment2d { type Output = Segment2dMeshBuilder; - fn mesh(&self) -> Self::Output { - Segment2dMeshBuilder::new(*self) - } -} - -impl From for Mesh { - /// Converts this segment into a [`Mesh`] using a default [`Segment2dMeshBuilder`]. - fn from(segment: Segment2d) -> Self { - segment.mesh().build() + fn mesh(self) -> Self::Output { + Segment2dMeshBuilder::new(self) } } @@ -712,16 +663,8 @@ impl MeshBuilder for Polyline2dMeshBuilder { impl Meshable for Polyline2d { type Output = Polyline2dMeshBuilder; - fn mesh(&self) -> Self::Output { - Polyline2dMeshBuilder { - polyline: self.clone(), - } - } -} - -impl From for Mesh { - fn from(polyline: Polyline2d) -> Self { - polyline.mesh().build() + fn mesh(self) -> Self::Output { + Polyline2dMeshBuilder { polyline: self } } } @@ -844,20 +787,14 @@ impl Extrudable for AnnulusMeshBuilder { impl Meshable for Annulus { type Output = AnnulusMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { AnnulusMeshBuilder { - annulus: *self, + annulus: self, ..Default::default() } } } -impl From for Mesh { - fn from(annulus: Annulus) -> Self { - annulus.mesh().build() - } -} - /// A builder for creating a [`Mesh`] with an [`Rhombus`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -931,19 +868,13 @@ impl Extrudable for RhombusMeshBuilder { impl Meshable for Rhombus { type Output = RhombusMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { Self::Output { half_diagonals: self.half_diagonals, } } } -impl From for Mesh { - fn from(rhombus: Rhombus) -> Self { - rhombus.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`Triangle2d`] shape. #[derive(Clone, Copy, Debug, Default, Reflect)] #[reflect(Default, Debug, Clone)] @@ -963,8 +894,8 @@ impl Triangle2dMeshBuilder { impl Meshable for Triangle2d { type Output = Triangle2dMeshBuilder; - fn mesh(&self) -> Self::Output { - Self::Output { triangle: *self } + fn mesh(self) -> Self::Output { + Self::Output { triangle: self } } } @@ -1015,12 +946,6 @@ impl Extrudable for Triangle2dMeshBuilder { } } -impl From for Mesh { - fn from(triangle: Triangle2d) -> Self { - triangle.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`Rectangle`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -1088,19 +1013,13 @@ impl Extrudable for RectangleMeshBuilder { impl Meshable for Rectangle { type Output = RectangleMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { RectangleMeshBuilder { half_size: self.half_size, } } } -impl From for Mesh { - fn from(rectangle: Rectangle) -> Self { - rectangle.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`Capsule2d`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -1248,20 +1167,14 @@ impl Extrudable for Capsule2dMeshBuilder { impl Meshable for Capsule2d { type Output = Capsule2dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { Capsule2dMeshBuilder { - capsule: *self, + capsule: self, ..Default::default() } } } -impl From for Mesh { - fn from(capsule: Capsule2d) -> Self { - capsule.mesh().build() - } -} - /// A builder used for creating a [`Mesh`] with a [`Ring`] shape. pub struct RingMeshBuilder

where @@ -1276,7 +1189,7 @@ where P: Primitive2d + Meshable, { /// Create a new `RingMeshBuilder

` from a given `Ring

` shape. - pub fn new(ring: &Ring

) -> Self { + pub fn new(ring: Ring

) -> Self { Self { outer_shape_builder: ring.outer_shape.mesh(), inner_shape_builder: ring.inner_shape.mesh(), @@ -1511,20 +1424,11 @@ where { type Output = RingMeshBuilder

; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { RingMeshBuilder::new(self) } } -impl

From> for Mesh -where - P: Primitive2d + Meshable, -{ - fn from(ring: Ring

) -> Self { - ring.mesh().build() - } -} - #[cfg(test)] mod tests { use bevy_math::{prelude::Annulus, primitives::RegularPolygon, FloatOrd}; diff --git a/crates/bevy_mesh/src/primitives/dim3/capsule.rs b/crates/bevy_mesh/src/primitives/dim3/capsule.rs index 64f5ae86ee6f3..13b937f05ca8e 100644 --- a/crates/bevy_mesh/src/primitives/dim3/capsule.rs +++ b/crates/bevy_mesh/src/primitives/dim3/capsule.rs @@ -421,16 +421,10 @@ impl MeshBuilder for Capsule3dMeshBuilder { impl Meshable for Capsule3d { type Output = Capsule3dMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { Capsule3dMeshBuilder { - capsule: *self, + capsule: self, ..Default::default() } } } - -impl From for Mesh { - fn from(capsule: Capsule3d) -> Self { - capsule.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/cone.rs b/crates/bevy_mesh/src/primitives/dim3/cone.rs index b773f05a71e74..7f4551768dd51 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cone.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cone.rs @@ -174,20 +174,14 @@ impl MeshBuilder for ConeMeshBuilder { impl Meshable for Cone { type Output = ConeMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { ConeMeshBuilder { - cone: *self, + cone: self, ..Default::default() } } } -impl From for Mesh { - fn from(cone: Cone) -> Self { - cone.mesh().build() - } -} - #[cfg(test)] mod tests { use crate::{Mesh, MeshBuilder, Meshable, VertexAttributeValues}; diff --git a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs index a0ca1fe9519ec..f6821bf638133 100644 --- a/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs +++ b/crates/bevy_mesh/src/primitives/dim3/conical_frustum.rs @@ -169,16 +169,10 @@ impl MeshBuilder for ConicalFrustumMeshBuilder { impl Meshable for ConicalFrustum { type Output = ConicalFrustumMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { ConicalFrustumMeshBuilder { - frustum: *self, + frustum: self, ..Default::default() } } } - -impl From for Mesh { - fn from(frustum: ConicalFrustum) -> Self { - frustum.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs index 7edd0c3d71945..de0cc0bcd93f7 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cuboid.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cuboid.rs @@ -85,15 +85,9 @@ impl MeshBuilder for CuboidMeshBuilder { impl Meshable for Cuboid { type Output = CuboidMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { CuboidMeshBuilder { half_size: self.half_size, } } } - -impl From for Mesh { - fn from(cuboid: Cuboid) -> Self { - cuboid.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs index dc8956243f5c5..575978ad313c4 100644 --- a/crates/bevy_mesh/src/primitives/dim3/cylinder.rs +++ b/crates/bevy_mesh/src/primitives/dim3/cylinder.rs @@ -207,16 +207,10 @@ impl MeshBuilder for CylinderMeshBuilder { impl Meshable for Cylinder { type Output = CylinderMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { CylinderMeshBuilder { - cylinder: *self, + cylinder: self, ..Default::default() } } } - -impl From for Mesh { - fn from(cylinder: Cylinder) -> Self { - cylinder.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/plane.rs b/crates/bevy_mesh/src/primitives/dim3/plane.rs index 3b174f5bb48f8..d696123774344 100644 --- a/crates/bevy_mesh/src/primitives/dim3/plane.rs +++ b/crates/bevy_mesh/src/primitives/dim3/plane.rs @@ -145,16 +145,10 @@ impl MeshBuilder for PlaneMeshBuilder { impl Meshable for Plane3d { type Output = PlaneMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { PlaneMeshBuilder { - plane: *self, + plane: self, subdivisions: 0, } } } - -impl From for Mesh { - fn from(plane: Plane3d) -> Self { - plane.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs index f0e8ae42b0c54..7690ce1a9e94e 100644 --- a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs @@ -29,15 +29,7 @@ impl MeshBuilder for Polyline3dMeshBuilder { impl Meshable for Polyline3d { type Output = Polyline3dMeshBuilder; - fn mesh(&self) -> Self::Output { - Polyline3dMeshBuilder { - polyline: self.clone(), - } - } -} - -impl From for Mesh { - fn from(polyline: Polyline3d) -> Self { - polyline.mesh().build() + fn mesh(self) -> Self::Output { + Polyline3dMeshBuilder { polyline: self } } } diff --git a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs index 4326955cb2324..60927ba7245b6 100644 --- a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs @@ -24,14 +24,8 @@ impl MeshBuilder for Segment3dMeshBuilder { impl Meshable for Segment3d { type Output = Segment3dMeshBuilder; - fn mesh(&self) -> Self::Output { - Segment3dMeshBuilder { segment: *self } - } -} - -impl From for Mesh { - fn from(segment: Segment3d) -> Self { - segment.mesh().build() + fn mesh(self) -> Self::Output { + Segment3dMeshBuilder { segment: self } } } diff --git a/crates/bevy_mesh/src/primitives/dim3/sphere.rs b/crates/bevy_mesh/src/primitives/dim3/sphere.rs index 946b850430262..3be960c6a0ed9 100644 --- a/crates/bevy_mesh/src/primitives/dim3/sphere.rs +++ b/crates/bevy_mesh/src/primitives/dim3/sphere.rs @@ -249,16 +249,10 @@ impl MeshBuilder for SphereMeshBuilder { impl Meshable for Sphere { type Output = SphereMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { SphereMeshBuilder { - sphere: *self, + sphere: self, ..Default::default() } } } - -impl From for Mesh { - fn from(sphere: Sphere) -> Self { - sphere.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs index 432aa4337c4a5..265223088ab88 100644 --- a/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs +++ b/crates/bevy_mesh/src/primitives/dim3/tetrahedron.rs @@ -54,13 +54,7 @@ impl MeshBuilder for TetrahedronMeshBuilder { impl Meshable for Tetrahedron { type Output = TetrahedronMeshBuilder; - fn mesh(&self) -> Self::Output { - TetrahedronMeshBuilder { tetrahedron: *self } - } -} - -impl From for Mesh { - fn from(tetrahedron: Tetrahedron) -> Self { - tetrahedron.mesh().build() + fn mesh(self) -> Self::Output { + TetrahedronMeshBuilder { tetrahedron: self } } } diff --git a/crates/bevy_mesh/src/primitives/dim3/torus.rs b/crates/bevy_mesh/src/primitives/dim3/torus.rs index d2267774c764a..8da04cc2b36a5 100644 --- a/crates/bevy_mesh/src/primitives/dim3/torus.rs +++ b/crates/bevy_mesh/src/primitives/dim3/torus.rs @@ -161,16 +161,10 @@ impl MeshBuilder for TorusMeshBuilder { impl Meshable for Torus { type Output = TorusMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { TorusMeshBuilder { - torus: *self, + torus: self, ..Default::default() } } } - -impl From for Mesh { - fn from(torus: Torus) -> Self { - torus.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs index 2337feb512ecd..64c941cb68fdc 100644 --- a/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/triangle3d.rs @@ -35,8 +35,8 @@ impl MeshBuilder for Triangle3dMeshBuilder { impl Meshable for Triangle3d { type Output = Triangle3dMeshBuilder; - fn mesh(&self) -> Self::Output { - Triangle3dMeshBuilder { triangle: *self } + fn mesh(self) -> Self::Output { + Triangle3dMeshBuilder { triangle: self } } } @@ -94,12 +94,6 @@ pub(crate) fn uv_coords(triangle: &Triangle3d) -> [[f32; 2]; 3] { } } -impl From for Mesh { - fn from(triangle: Triangle3d) -> Self { - triangle.mesh().build() - } -} - #[cfg(test)] mod tests { use super::uv_coords; diff --git a/crates/bevy_mesh/src/primitives/extrusion.rs b/crates/bevy_mesh/src/primitives/extrusion.rs index 2670805080904..3dc9a4593923e 100644 --- a/crates/bevy_mesh/src/primitives/extrusion.rs +++ b/crates/bevy_mesh/src/primitives/extrusion.rs @@ -92,7 +92,7 @@ where { type Output = ExtrusionBuilder

; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { ExtrusionBuilder { base_builder: self.base_shape.mesh(), half_depth: self.half_depth, @@ -118,7 +118,7 @@ where P::Output: Extrudable, { /// Create a new `ExtrusionBuilder

` from a given `base_shape` and the full `depth` of the extrusion. - pub fn new(base_shape: &P, depth: f32) -> Self { + pub fn new(base_shape: P, depth: f32) -> Self { Self { base_builder: base_shape.mesh(), half_depth: depth / 2., @@ -425,13 +425,3 @@ where front_face } } - -impl

From> for Mesh -where - P: Primitive2d + Meshable, - P::Output: Extrudable, -{ - fn from(value: Extrusion

) -> Self { - value.mesh().build() - } -} diff --git a/crates/bevy_mesh/src/primitives/mod.rs b/crates/bevy_mesh/src/primitives/mod.rs index 64f0ab16a1242..74655ae5087bd 100644 --- a/crates/bevy_mesh/src/primitives/mod.rs +++ b/crates/bevy_mesh/src/primitives/mod.rs @@ -38,7 +38,7 @@ pub trait Meshable { type Output: MeshBuilder; /// Creates a [`Mesh`] for a shape. - fn mesh(&self) -> Self::Output; + fn mesh(self) -> Self::Output; } /// A trait used to build [`Mesh`]es from a configuration @@ -52,14 +52,22 @@ pub trait MeshBuilder { } } -impl From for Mesh { - fn from(builder: T) -> Self { - builder.build() +impl Meshable for T { + type Output = Self; + + fn mesh(self) -> Self::Output { + self + } +} + +impl From for Mesh { + fn from(meshable: T) -> Self { + meshable.mesh().build() } } -impl From for InfallibleMesh { - fn from(builder: T) -> Self { - builder.build_infallible() +impl From for InfallibleMesh { + fn from(meshable: T) -> Self { + meshable.mesh().build_infallible() } } diff --git a/examples/math/custom_primitives.rs b/examples/math/custom_primitives.rs index a24f033701000..2315c054041df 100644 --- a/examples/math/custom_primitives.rs +++ b/examples/math/custom_primitives.rs @@ -472,9 +472,9 @@ impl Meshable for Heart { // The `MeshBuilder` can be used to create the actual mesh for that primitive. type Output = HeartMeshBuilder; - fn mesh(&self) -> Self::Output { + fn mesh(self) -> Self::Output { Self::Output { - heart: *self, + heart: self, resolution: 32, } } diff --git a/examples/math/sampling_primitives.rs b/examples/math/sampling_primitives.rs index 75cbc77118d34..75bb370a81116 100644 --- a/examples/math/sampling_primitives.rs +++ b/examples/math/sampling_primitives.rs @@ -201,8 +201,8 @@ impl ShapeSample for Shape { impl Meshable for Shape { type Output = ShapeMeshBuilder; - fn mesh(&self) -> Self::Output { - ShapeMeshBuilder { shape: *self } + fn mesh(self) -> Self::Output { + ShapeMeshBuilder { shape: self } } } From ab9a52aa63bbb65d3520a9abb790f7de613dd741 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:03:06 +0000 Subject: [PATCH 132/133] remove Asset derive --- crates/bevy_mesh/src/mesh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 65930d470f27b..49937912b3755 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -137,7 +137,7 @@ impl From> for MeshExtractableData { /// that can be converted into a normal `Mesh` using `InfallibleMesh::into_mesh`. /// Designed for use during cpu-side mesh construction. Methods on this struct whose `Mesh` /// counterparts may return `MeshAccessError::ExtractedToRenderWorld` instead return plain values. -#[derive(Asset, Debug, Clone, Reflect, PartialEq)] +#[derive(Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] pub struct InfallibleMesh(Mesh); From 7d6aae4c885039c18e9ec3ff214d8a795ff18c03 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Tue, 16 Dec 2025 00:06:36 +0000 Subject: [PATCH 133/133] fix new example --- examples/3d/clustered_decal_maps.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/3d/clustered_decal_maps.rs b/examples/3d/clustered_decal_maps.rs index 0f88747fd1fd9..31b07d58fb84c 100644 --- a/examples/3d/clustered_decal_maps.rs +++ b/examples/3d/clustered_decal_maps.rs @@ -197,7 +197,7 @@ fn spawn_plane_mesh( half_size: Vec2::splat(PLANE_HALF_SIZE), } .mesh() - .build() + .build_infallible() .with_duplicated_vertices() .with_computed_flat_normals() .with_generated_tangents()